From 631f7fb68fb1bc58c9cb021e185d9f89b210bc4e Mon Sep 17 00:00:00 2001 From: naaz-josh Date: Thu, 4 Sep 2025 18:38:45 +0530 Subject: [PATCH 01/10] Added Dropdown with navigation --- app/src/app/crm/page.tsx | 22 ++++++++++++++++++++ app/src/components/Navigation.tsx | 34 ++++++++++++++++++++++++++----- 2 files changed, 51 insertions(+), 5 deletions(-) create mode 100644 app/src/app/crm/page.tsx diff --git a/app/src/app/crm/page.tsx b/app/src/app/crm/page.tsx new file mode 100644 index 0000000..577ac70 --- /dev/null +++ b/app/src/app/crm/page.tsx @@ -0,0 +1,22 @@ +import { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Lingo.ai | CRM", +}; + +const CRMPage = () => { + return ( +
+
+

CRM Dashboard

+
+

+ CRM functionality will be implemented here. This is a placeholder page for the View CRM feature. +

+
+
+
+ ); +}; + +export default CRMPage; diff --git a/app/src/components/Navigation.tsx b/app/src/components/Navigation.tsx index 7addf1b..ed47c54 100644 --- a/app/src/components/Navigation.tsx +++ b/app/src/components/Navigation.tsx @@ -14,6 +14,9 @@ import { Files, User2, Layers, + ChevronDown, + FileText, + Database, } from "lucide-react"; import { DropdownMenu, @@ -239,11 +242,32 @@ const Navigation = ({ isSignedIn }: NavigationProps) => { )} - {pathname !== "/transcriptions" && ( - + {pathname !== "/transcriptions" && pathname !== "/crm" && ( + + + + + + router.push("/transcriptions")} + className="cursor-pointer hover:!text-black hover:font-bold hover:!bg-[#668D7E]/30" + > + + Summarization + + router.push("/crm")} + className="cursor-pointer hover:!text-black hover:font-bold hover:!bg-[#668D7E]/30" + > + + View CRM + + + )} {isSignedIn && pathname !== "/" && ( From ef379002e86e6dbe8d71c49f98ad1e7f10ee61b7 Mon Sep 17 00:00:00 2001 From: arpita-josh02 Date: Sat, 6 Sep 2025 00:06:12 +0530 Subject: [PATCH 02/10] crm changes --- app/migrations/0012_productive_wallop.sql | 34 ++ app/migrations/meta/0012_snapshot.json | 578 ++++++++++++++++++ app/migrations/meta/_journal.json | 7 + app/src/app/api/crm-leads/default/route.ts | 26 + app/src/app/api/crm-leads/route.ts | 37 ++ .../app/api/crm-leads/simple/[id]/route.ts | 53 ++ app/src/app/api/transcribe/save/route.ts | 34 +- app/src/components/RecorderCard.tsx | 8 +- app/src/db/schema.ts | 22 + app/src/types/TranscriptionResponse.ts | 5 + service/config.py | 10 +- service/crm_client.py | 94 +++ service/main.py | 274 +++++++-- service/summarizer.py | 185 +++++- 14 files changed, 1311 insertions(+), 56 deletions(-) create mode 100644 app/migrations/0012_productive_wallop.sql create mode 100644 app/migrations/meta/0012_snapshot.json create mode 100644 app/src/app/api/crm-leads/default/route.ts create mode 100644 app/src/app/api/crm-leads/route.ts create mode 100644 app/src/app/api/crm-leads/simple/[id]/route.ts create mode 100644 service/crm_client.py diff --git a/app/migrations/0012_productive_wallop.sql b/app/migrations/0012_productive_wallop.sql new file mode 100644 index 0000000..8ce9852 --- /dev/null +++ b/app/migrations/0012_productive_wallop.sql @@ -0,0 +1,34 @@ +CREATE TABLE IF NOT EXISTS "crm_leads" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "lead_id" text NOT NULL, + "crm_url" text NOT NULL, + "file_name" text NOT NULL, + "document_url" text NOT NULL, + "transcription_id" uuid, + "extracted_data" jsonb NOT NULL, + "translation" text NOT NULL, + "user_id" text, + "is_default" boolean DEFAULT false NOT NULL, + "createdAt" timestamp DEFAULT now() +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "password_reset_tokens" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "username" text NOT NULL, + "token" text NOT NULL, + "expires_at" timestamp NOT NULL, + "createdAt" timestamp DEFAULT now(), + CONSTRAINT "password_reset_tokens_username_unique" UNIQUE("username") +); +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "crm_leads" ADD CONSTRAINT "crm_leads_transcription_id_transcriptions_id_fk" FOREIGN KEY ("transcription_id") REFERENCES "public"."transcriptions"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "crm_leads" ADD CONSTRAINT "crm_leads_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/app/migrations/meta/0012_snapshot.json b/app/migrations/meta/0012_snapshot.json new file mode 100644 index 0000000..d376601 --- /dev/null +++ b/app/migrations/meta/0012_snapshot.json @@ -0,0 +1,578 @@ +{ + "id": "31665b6b-8fda-4ba7-9342-a12b1c367588", + "prevId": "51c1645a-26bf-4446-97a1-c2049cca0522", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.bot": { + "name": "bot", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "botName": { + "name": "botName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "botEmail": { + "name": "botEmail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "botHd": { + "name": "botHd", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "botPicture": { + "name": "botPicture", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessToken": { + "name": "accessToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "bot_user_id_user_id_fk": { + "name": "bot_user_id_user_id_fk", + "tableFrom": "bot", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.crm_leads": { + "name": "crm_leads", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "lead_id": { + "name": "lead_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "crm_url": { + "name": "crm_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_url": { + "name": "document_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "transcription_id": { + "name": "transcription_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "extracted_data": { + "name": "extracted_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "translation": { + "name": "translation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "crm_leads_transcription_id_transcriptions_id_fk": { + "name": "crm_leads_transcription_id_transcriptions_id_fk", + "tableFrom": "crm_leads", + "tableTo": "transcriptions", + "columnsFrom": [ + "transcription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "crm_leads_user_id_user_id_fk": { + "name": "crm_leads_user_id_user_id_fk", + "tableFrom": "crm_leads", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.password_reset_tokens": { + "name": "password_reset_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "password_reset_tokens_username_unique": { + "name": "password_reset_tokens_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + } + }, + "public.registrations": { + "name": "registrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userName": { + "name": "userName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userEmail": { + "name": "userEmail", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.subscriptions": { + "name": "subscriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recordingCount": { + "name": "recordingCount", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "fileSizeLimitMB": { + "name": "fileSizeLimitMB", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "durationDays": { + "name": "durationDays", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "subscriptions_name_unique": { + "name": "subscriptions_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + } + }, + "public.transcriptions": { + "name": "transcriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "translation": { + "name": "translation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "segments": { + "name": "segments", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "detected_language": { + "name": "detected_language", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "documentUrl": { + "name": "documentUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "documentName": { + "name": "documentName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "isDefault": { + "name": "isDefault", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "audioDuration": { + "name": "audioDuration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_name": { + "name": "user_name", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "transcriptions_user_id_user_id_fk": { + "name": "transcriptions_user_id_user_id_fk", + "tableFrom": "transcriptions", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contactNumber": { + "name": "contactNumber", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subscriptionId": { + "name": "subscriptionId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_subscriptionId_subscriptions_id_fk": { + "name": "user_subscriptionId_subscriptions_id_fk", + "tableFrom": "user", + "tableTo": "subscriptions", + "columnsFrom": [ + "subscriptionId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_username_unique": { + "name": "user_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + } + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/app/migrations/meta/_journal.json b/app/migrations/meta/_journal.json index 54d72fe..4502f5e 100644 --- a/app/migrations/meta/_journal.json +++ b/app/migrations/meta/_journal.json @@ -85,6 +85,13 @@ "when": 1754562354517, "tag": "0011_lucky_genesis", "breakpoints": true + }, + { + "idx": 12, + "version": "7", + "when": 1757093957998, + "tag": "0012_productive_wallop", + "breakpoints": true } ] } \ No newline at end of file diff --git a/app/src/app/api/crm-leads/default/route.ts b/app/src/app/api/crm-leads/default/route.ts new file mode 100644 index 0000000..51a093a --- /dev/null +++ b/app/src/app/api/crm-leads/default/route.ts @@ -0,0 +1,26 @@ +import { NextResponse } from "next/server"; +import { db } from "@/db"; +import { crmLeadsTable } from "@/db/schema"; +import { eq } from "drizzle-orm"; + +export async function GET(req: Request) { + try { + // Get the default CRM leads + const defaultLeads = await db + .select() + .from(crmLeadsTable) + .where(eq(crmLeadsTable.isDefault, true)); + + // Return the results + return NextResponse.json({ + success: true, + data: defaultLeads + }); + } catch (error) { + console.error("Error fetching default CRM leads:", error); + return NextResponse.json( + { success: false, error: "Failed to fetch default CRM leads" }, + { status: 500 } + ); + } +} diff --git a/app/src/app/api/crm-leads/route.ts b/app/src/app/api/crm-leads/route.ts new file mode 100644 index 0000000..1ffcc15 --- /dev/null +++ b/app/src/app/api/crm-leads/route.ts @@ -0,0 +1,37 @@ +import { NextResponse } from "next/server"; +import { db } from "@/db"; +import { crmLeadsTable } from "@/db/schema"; + +export async function GET() { + try { + console.log("Fetching all CRM leads"); + + // Query all CRM leads + const allLeads = await db + .select() + .from(crmLeadsTable); + + console.log(`Found ${allLeads.length} CRM leads`); + + if (allLeads.length > 0) { + console.log("Sample lead data:", { + id: allLeads[0].id, + leadId: allLeads[0].leadId, + fileName: allLeads[0].fileName + }); + } + + // Return the leads, even if empty + return NextResponse.json({ + success: true, + data: allLeads, + count: allLeads.length + }); + } catch (error) { + console.error("Error retrieving CRM leads:", error); + return NextResponse.json( + { success: false, error: String(error) }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/src/app/api/crm-leads/simple/[id]/route.ts b/app/src/app/api/crm-leads/simple/[id]/route.ts new file mode 100644 index 0000000..349242d --- /dev/null +++ b/app/src/app/api/crm-leads/simple/[id]/route.ts @@ -0,0 +1,53 @@ +import { NextResponse } from "next/server"; +import { db } from "@/db"; +import { crmLeadsTable } from "@/db/schema"; +import { eq, or } from "drizzle-orm"; + +export async function GET( + req: Request, + { params }: { params: { id: string } } +) { + try { + const id = params.id; + + console.log("Direct lookup for CRM lead with ID:", id); + + if (!id) { + return NextResponse.json({ error: "ID is required" }, { status: 400 }); + } + + // Try to match by any ID field + const lead = await db + .select() + .from(crmLeadsTable) + .where( + or( + eq(crmLeadsTable.id, id), + eq(crmLeadsTable.leadId, id) + ) + ) + .limit(1); + + // If no leads are found, check if the table has any data at all + if (!lead || lead.length === 0) { + const count = await db + .select({ count: db.fn.count() }) + .from(crmLeadsTable); + + const totalCount = Number(count[0]?.count || 0); + + return NextResponse.json({ + error: "CRM lead not found", + id, + tableHasData: totalCount > 0, + totalLeads: totalCount + }, { status: 404 }); + } + + // Return the found lead + return NextResponse.json(lead[0]); + } catch (error) { + console.error("Error in direct CRM lead lookup:", error); + return NextResponse.json({ error: String(error) }, { status: 500 }); + } +} \ No newline at end of file diff --git a/app/src/app/api/transcribe/save/route.ts b/app/src/app/api/transcribe/save/route.ts index bbecb05..e7c883f 100644 --- a/app/src/app/api/transcribe/save/route.ts +++ b/app/src/app/api/transcribe/save/route.ts @@ -1,6 +1,7 @@ import { db } from "@/db"; import { transcriptions, TranscriptionsPayload, userTable } from "@/db/schema"; import { eq } from "drizzle-orm"; +import { crmLeadsTable } from "@/db/schema"; // Import crmLeadsTable explicitly export async function POST(req: Request) { try { @@ -13,8 +14,11 @@ export async function POST(req: Request) { translation, audioDuration, segments, - detectedLanguage - }: TranscriptionsPayload = body; + detectedLanguage, + isDefault, + } = body; + // CRM data + const { leadId, crmUrl, extractedData } = body; @@ -42,10 +46,34 @@ export async function POST(req: Request) { segments, detectedLanguage }).returning(); + + // If lead ID exists, save CRM data + if (leadId) { + try { + console.log("Saving CRM lead data for lead ID:", leadId); + + await db.insert(crmLeadsTable).values({ + leadId: leadId, + crmUrl: crmUrl || "", + fileName: documentName, + documentUrl: documentUrl, // Make sure this field exists in your schema + transcriptionId: response[0].id, + extractedData: extractedData || {}, + translation: translation, + userId: userID, + isDefault: isDefault === true + }); + + console.log("CRM lead data saved successfully"); + } catch (crmError) { + console.error("Error saving CRM lead data:", crmError); + // Continue with the response even if CRM save fails + } + } return new Response(JSON.stringify(response), { status: 200 }); } catch (error) { console.log(error); return new Response(JSON.stringify(error), { status: 500 }); } -} +} \ No newline at end of file diff --git a/app/src/components/RecorderCard.tsx b/app/src/components/RecorderCard.tsx index c5c69ab..63d212d 100644 --- a/app/src/components/RecorderCard.tsx +++ b/app/src/components/RecorderCard.tsx @@ -117,6 +117,10 @@ const RecorderCard = (props: RecorderCardProps) => { translation: res.translation, segments: res.segments, detectedLanguage: res.detected_language, + leadId: res.leadId, + crmUrl: res.crmUrl, + extractedData: res.extractedData, + isDefault: res.isDefault, }); }, onError: (error) => { @@ -181,7 +185,7 @@ const RecorderCard = (props: RecorderCardProps) => { setStatus(`Saving transcription for ${file?.name}`); toast.info(`Saving transcription for ${file?.name}`); }, - mutationFn: async (data: TranscriptionsPayload) => { + mutationFn: async (data: any) => { if (recordingTime > 0) { // recorded data.audioDuration = recordingTime; @@ -437,4 +441,4 @@ const RecorderCard = (props: RecorderCardProps) => { ); }; -export default RecorderCard; +export default RecorderCard; \ No newline at end of file diff --git a/app/src/db/schema.ts b/app/src/db/schema.ts index 0a0e1d6..132849f 100644 --- a/app/src/db/schema.ts +++ b/app/src/db/schema.ts @@ -97,3 +97,25 @@ export const passwordResetTokens = pgTable("password_reset_tokens", { export type TranscriptionsPayload = typeof transcriptions.$inferInsert; export type TranscriptionsType = typeof transcriptions.$inferSelect; + + + +// Add this to your schema.ts file if it doesn't exist + +export const crmLeadsTable = pgTable("crm_leads", { + id: uuid("id").primaryKey().defaultRandom(), + leadId: text("lead_id").notNull(), + crmUrl: text("crm_url").notNull(), + fileName: text("file_name").notNull(), + documentUrl: text("document_url").notNull(), // Add this new field + transcriptionId: uuid("transcription_id").references(() => transcriptions.id), + extractedData: jsonb("extracted_data").notNull(), + translation: text("translation").notNull(), + userId: text("user_id").references(() => userTable.id), + isDefault: boolean("is_default").notNull().default(false), + createdAt: timestamp("createdAt", { mode: "date" }).defaultNow() +}); + +// Make sure to export the type as well +export type CrmLeadsPayload = typeof crmLeadsTable.$inferInsert; +export type CrmLeadsType = typeof crmLeadsTable.$inferSelect; \ No newline at end of file diff --git a/app/src/types/TranscriptionResponse.ts b/app/src/types/TranscriptionResponse.ts index fedc66a..c84c646 100644 --- a/app/src/types/TranscriptionResponse.ts +++ b/app/src/types/TranscriptionResponse.ts @@ -6,4 +6,9 @@ export type TranscriptionResponse = { summary: string; segments: segment[]; detected_language: string; + leadId?: string; + crmUrl?: string; + extractedData?: any; + isDefault?: boolean; + transcriptionId?: string | null; }; diff --git a/service/config.py b/service/config.py index 8bba447..f871898 100644 --- a/service/config.py +++ b/service/config.py @@ -5,9 +5,11 @@ openai_api_key = os.getenv("OPENAI_API_KEY") #model_id = os.getenv('MODEL_ID', 'large-v3') -model_id = os.getenv('MODEL_ID','small') -model_path = os.getenv('MODEL_PATH', './models') +model_id = os.getenv('MODEL_ID') +model_path = os.getenv('MODEL_PATH') ollama_host = os.getenv("OLLAMA_HOST", "http://ollama:11434") ollama_model_name = os.getenv("OLLAMA_MODEL_NAME", "llama3.2") -open_ai_model_name = os.getenv("OPENAI_MODEL_NAME", "gpt-4") -open_ai_temperature = os.getenv("OPENAI_TEMPERATURE", 0.2) +odoo_url = os.getenv("ODOO_URL") +odoo_db = os.getenv("ODOO_DB") +odoo_username = os.getenv("ODOO_USERNAME") +odoo_password = os.getenv("ODOO_PASSWORD") \ No newline at end of file diff --git a/service/crm_client.py b/service/crm_client.py new file mode 100644 index 0000000..5b88a45 --- /dev/null +++ b/service/crm_client.py @@ -0,0 +1,94 @@ +import xmlrpc.client +from typing import Optional + + +class OdooCRMClient: + def __init__(self, url: str, db: str, username: str, password: str): + self.url = url + self.db = db + self.username = username + self.password = password + + + self.common = xmlrpc.client.ServerProxy(f"{url}/xmlrpc/2/common", allow_none=True) + self.uid = self.common.authenticate(db, username, password, {}) + if not self.uid: + raise Exception("Authentication failed. Check credentials or DB name.") + self.models = xmlrpc.client.ServerProxy(f"{url}/xmlrpc/2/object", allow_none=True) + + + def create_lead(self, name: str, email: Optional[str], phone: Optional[str], lead_type: str = "opportunity"): + lead_id = self.models.execute_kw( + self.db, self.uid, self.password, + "crm.lead", "create", + [{ + "name": name, + "contact_name": name, + "email_from": email, + "phone": phone, + "type": lead_type, + }] + ) + return lead_id + + def update_lead(self, lead_id: int, vals: dict): + return self.models.execute_kw( + self.db, self.uid, self.password, + "crm.lead", "write", + [[lead_id], vals] + ) + + def add_internal_note(self, lead_id: int, note_text: str): + return self.update_lead(lead_id, {"description": note_text}) + + def add_chatter_note(self, lead_id: int, note_text: str): + return self.models.execute_kw( + self.db, self.uid, self.password, + "mail.message", "create", + [{ + "model": "crm.lead", + "res_id": lead_id, + "body": note_text, + "message_type": "comment", + "subtype_id": 2, + }] + ) + + def add_contact_details(self, lead_id: int, contact_name: Optional[str] = None, email: Optional[str] = None, phone: Optional[str] = None): + vals = {} + if contact_name: + vals["name"] = contact_name + if email: + vals["email"] = email + if phone: + vals["phone"] = phone + + partner_id = self.models.execute_kw( + self.db, self.uid, self.password, + "res.partner", "create", + [vals] + ) + + self.update_lead(lead_id, {"partner_id": partner_id}) + return partner_id + + def update_contact_address(self, partner_id: int, street: Optional[str] = None, street2: Optional[str] = None, city: Optional[str] = None, state_id: Optional[int] = None, zip_code: Optional[str] = None, country_id: Optional[int] = None): + vals = {} + if street: + vals["street"] = street + if street2: + vals["street2"] = street2 + if city: + vals["city"] = city + if state_id: + vals["state_id"] = state_id + if zip_code: + vals["zip"] = zip_code + if country_id: + vals["country_id"] = country_id + + return self.models.execute_kw( + self.db, self.uid, self.password, + "res.partner", "write", + [[partner_id], vals] + ) \ No newline at end of file diff --git a/service/main.py b/service/main.py index c3a7a34..3514ca2 100644 --- a/service/main.py +++ b/service/main.py @@ -1,19 +1,23 @@ -from fastapi import FastAPI, UploadFile, File, Form +from fastapi import FastAPI, HTTPException from fastapi.responses import JSONResponse from logger import logger from dotenv import load_dotenv from starlette.middleware.cors import CORSMiddleware from audio_service import translate_with_whisper -from audio_service import translate_with_whisper_timestamped, translate_with_whisper_from_upload -from intent import find_intent_using_openai, find_intent_using_regex +from audio_service import translate_with_whisper_timestamped from summarizer import summarize_using_openai -from summarizer import summarize_using_ollama +from summarizer import summarize_using_ollama, extract_contact_detailed_using_ollama from pydantic import BaseModel import traceback from util import generate_timestamp_json from fastapi_versionizer.versionizer import Versionizer, api_version +from config import odoo_url, odoo_db, odoo_username, odoo_password +from crm_client import OdooCRMClient +import os +import requests import json +import os.path app = FastAPI() # Add CORS middleware to the application @@ -31,17 +35,7 @@ def root_route(): class Body(BaseModel): audio_file_link: str - -def generate_timestamp_json(translation, summary, detected_language=None): - """Generate the final JSON response with all required fields""" - return { - "message": "File processed successfully!", - "translation": translation.get("text", ""), - "summary": summary, - "segments": translation.get("segments", []), - "detected_language": detected_language or translation.get("detected_language", "unknown") - } - +# First API endpoint (v1) @api_version(1) @app.post("/upload-audio") async def upload_audio(body: Body): @@ -49,25 +43,75 @@ async def upload_audio(body: Body): if body.audio_file_link == "": return JSONResponse(status_code=400, content={"message":"Invalid file link"}) + # Remove file extension check since frontend handles this translation = translate_with_whisper_timestamped(body.audio_file_link) - - # Extract detected language - detected_language = translation.get("detected_language", "unknown") - + detected_language = translation.get('detected_language', 'unknown') logger.info("translation done") summary = summarize_using_ollama(translation["text"]) logger.info("summary done") + result = generate_timestamp_json(translation,summary,detected_language) + + contact_info = extract_contact_detailed_using_ollama(translation["text"]) if "text" in translation else {"name": None, "phone": None, "address": None} - # Pass the translation object and detected_language to generate_timestamp_json - result = generate_timestamp_json(translation, summary, detected_language) - + logger.info(result) + + # Fire-and-forget CRM sync (do not block response) + lead_id = None + try: + if odoo_url and odoo_db and odoo_username and odoo_password: + client = OdooCRMClient(odoo_url, odoo_db, odoo_username, odoo_password) + lead_id = client.create_lead( + name=contact_info.get("name") or "Unknown", + email=None, + phone=contact_info.get("phone"), + ) + logger.info(f"CRM: Lead created lead_id={lead_id}") + partner_id = client.add_contact_details(lead_id, contact_info.get("name"), None, contact_info.get("phone")) + logger.info(f"CRM: Partner created/linked partner_id={partner_id} to lead_id={lead_id}") + + # Update: Use the complete street information from LLM extraction + street = contact_info.get("street") + logger.info(f"{street}= streetstreetstreetstreet") + street2 = None + city = contact_info.get("city") + state = contact_info.get("state") + zip_code = contact_info.get("zip") + country = contact_info.get("country") + + logger.info(f"CRM: Address components street={street}, city={city}") + + updated = client.update_contact_address( + partner_id, + street=street, + street2=street2, + city=city, + state_id=None, + zip_code=zip_code, + country_id=None + ) + logger.info(f"CRM: Address update result={updated} for partner_id={partner_id}") + + # Add CRM data to the result + result["leadId"] = str(lead_id) + result["crmUrl"] = odoo_url + result["extractedData"] = contact_info + result["transcriptionId"] = None # This will be determined when transcription is saved + result["translation"] = translation["text"] + result["userId"] = None # This will be set by frontend + result["isDefault"] = False + else: + logger.info("CRM: Odoo credentials not configured; skipping CRM sync") + except Exception as e: + logger.info(f"CRM: Exception during sync: {e}") return JSONResponse(content=result, status_code=200) except Exception as e: logger.info(traceback.format_exc()) return JSONResponse(content={"message": str(e)}, status_code=500) + +# Second API endpoint (v2) @api_version(2) @app.post("/upload-audio") async def upload_audio(body: Body): @@ -84,6 +128,64 @@ async def upload_audio(body: Body): logger.info("summary done") result = generate_timestamp_json(translation,summary,detected_language) + contact_info = extract_contact_detailed_using_ollama(translation["text"]) if "text" in translation else {"name": None, "phone": None, "address": None} + + logger.info(result) + + # Fire-and-forget CRM sync (do not block response) + lead_id = None + try: + print(odoo_url, odoo_db, odoo_username, odoo_password) + if odoo_url and odoo_db and odoo_username and odoo_password: + client = OdooCRMClient(odoo_url, odoo_db, odoo_username, odoo_password) + lead_id = client.create_lead( + name=contact_info.get("name") or "Unknown", + email=None, + phone=contact_info.get("phone"), + ) + logger.info(f"CRM: Lead created lead_id={lead_id}") + partner_id = client.add_contact_details(lead_id, contact_info.get("name"), None, contact_info.get("phone")) + logger.info(f"CRM: Partner created/linked partner_id={partner_id} to lead_id={lead_id}") + + # Update: Use the complete street information from LLM extraction + street = contact_info.get("street") + logger.info(f"CRM: - Street: '{street}'") + + + street2 = None + city = contact_info.get("city") + state = contact_info.get("state") + zip_code = contact_info.get("zip") + country = contact_info.get("country") + + logger.info(f"CRM: Address components street={street}, city={city}") + + updated = client.update_contact_address( + partner_id, + street=street, + street2=street2, + city=city, + state_id=None, + zip_code=zip_code, + country_id=None + ) + logger.info(f"CRM: Address update result={updated} for partner_id={partner_id}") + + # Add CRM data to the result + result["leadId"] = str(lead_id) + result["crmUrl"] = odoo_url + result["extractedData"] = contact_info + result["transcriptionId"] = None # This will be determined when transcription is saved + result["translation"] = translation["text"] + result["userId"] = None # This will be set by frontend + result["isDefault"] = False + + + else: + logger.info("CRM: Odoo credentials not configured; skipping CRM sync") + except Exception as e: + logger.info(f"CRM: Exception during sync: {e}") + return JSONResponse(content=result, status_code=200) except Exception as e: @@ -96,36 +198,116 @@ async def upload_audio(body: Body): latest_prefix='/latest', sort_routes=True ).versionize() +# Add this function to call our new API endpoint +async def save_crm_lead_data(lead_id, file_path, translation, extracted_data, summary, user_id=None, transcription_id=None): -@app.post("/voice/transcribe-intent") -async def transcribe_intent(audio: UploadFile = File(...), session_id: str = Form(...)): + """ + Save CRM lead data to the database through the Next.js API. + """ try: - if not audio: - return JSONResponse(status_code=400, content={"message":"No audio file provided"}) + # Get base URL from environment or use default + api_base_url = os.environ.get("NEXT_API_BASE_URL", "http://localhost:3000") + + # Extract just the filename from the file path + file_name = os.path.basename(file_path) + + # Prepare the data to send + crm_lead_data = { + "leadId": str(lead_id), + "crmUrl": odoo_url, # Using the odoo_url from config + "fileName": file_name, + "transcriptionId": transcription_id, # This might be None if not provided + "extractedData": extracted_data, + "translation": translation, + "userId": user_id, # This might be None if not provided + "isDefault": False # Adding the default field set to false + } + + # Make the API call + response = requests.post( + f"{api_base_url}/api/crm-leads", + json=crm_lead_data, + headers={"Content-Type": "application/json"} + ) + + if response.status_code == 200: + logger.info(f"CRM lead data saved successfully for lead_id={lead_id}") + return response.json() + else: + logger.error(f"Failed to save CRM lead data: {response.status_code} - {response.text}") + return None + + except Exception as e: + logger.error(f"Exception saving CRM lead data: {str(e)}") + return None - translation_text = translate_with_whisper_from_upload(audio) - logger.info("translation done") - intent = find_intent_using_regex(translation_text) - logger.info("intent find done") - try: - if isinstance(intent, dict): - intent_dict = intent - else: - intent_dict = json.loads(intent) - except json.JSONDecodeError: - logger.warning(f"Intent detection returned non-JSON response: {intent}") - result = {"error": intent, "session_id": session_id, "translation": translation_text} - return JSONResponse(content=result, status_code=200) +# Add this function to retrieve default CRM leads +async def get_default_crm_leads(): + """ + Fetch CRM leads that are marked as default. + """ + try: + api_base_url = os.environ.get("NEXT_API_BASE_URL", "http://localhost:3000") - result = { - "session_id": session_id, - "translation": translation_text, - "intent_data": intent_dict - } - return JSONResponse(content=result, status_code=200) + response = requests.get( + f"{api_base_url}/api/crm-leads/default", + headers={"Content-Type": "application/json"} + ) + + if response.status_code == 200: + return response.json() + else: + logger.error(f"Failed to get default CRM leads: {response.status_code}") + return None + except Exception as e: + logger.error(f"Error fetching default CRM leads: {str(e)}") + return None +# Add a route to expose this functionality +@app.get("/crm-leads/default") +async def fetch_default_crm_leads(): + try: + result = await get_default_crm_leads() + if result and result.get("success"): + return JSONResponse(content=result, status_code=200) + else: + return JSONResponse( + content={"message": "Failed to retrieve default CRM leads"}, + status_code=500 + ) except Exception as e: - logger.info(traceback.format_exc()) + logger.error(f"Error in fetch_default_crm_leads: {str(e)}") return JSONResponse(content={"message": str(e)}, status_code=500) +# Add this new, simpler endpoint + +@app.get("/crm-lead/{lead_id}") +async def get_crm_lead_direct(lead_id: str): + """ + Get CRM lead data directly by ID from the database. + Simple direct lookup without complex routing. + """ + try: + api_base_url = os.environ.get("NEXT_API_BASE_URL", "http://localhost:3000") + + # Make a direct GET request to a simple endpoint + simple_url = f"{api_base_url}/api/crm-leads/simple/{lead_id}" + logger.info(f"Making GET request to: {simple_url}") + + response = requests.get( + simple_url, + headers={"Content-Type": "application/json"} + ) + + if response.status_code == 200: + return JSONResponse(content=response.json(), status_code=200) + else: + return JSONResponse( + content={"message": "CRM lead not found", "id": lead_id}, + status_code=404 + ) + + except Exception as e: + logger.error(f"Error retrieving CRM lead: {str(e)}") + return JSONResponse(content={"message": str(e)}, status_code=500) \ No newline at end of file diff --git a/service/summarizer.py b/service/summarizer.py index 46bd015..7dda357 100644 --- a/service/summarizer.py +++ b/service/summarizer.py @@ -5,7 +5,8 @@ import logging import ollama from config import ollama_host, ollama_model_name - +import re +import json logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -45,3 +46,185 @@ def summarize_using_ollama(text): response = ollama.Client(host=ollama_host).generate(model=ollama_model_name, prompt = text+"\n \n""Provide highlights above conversation in Markdown bullet points, ready for direct inclusion in a file, with no pretext, and formatted as a multiline string.") summary = response["response"] return summary + +def _extract_first_json_block(text: str) -> dict | None: + """Helper to find the first JSON block in a string.""" + try: + start_index = text.find('{') + end_index = text.rfind('}') + if start_index != -1 and end_index != -1 and end_index > start_index: + json_str = text[start_index:end_index + 1] + return json.loads(json_str) + except json.JSONDecodeError: + return None + return None + +def extract_contact_detailed_using_ollama(text: str): + """Use LLM to extract detailed contact and address fields for CRM.""" + if not text or not text.strip(): + return {"name": None, "phone": None, "address": None, "street": None, "city": None, "state": None, "zip": None, "country": None} + + # Try to extract name using regex first as a fallback + name_pattern = r'(?:this is|my name is|I am|I\'m) ([A-Z][a-z]+ [A-Z][a-z]+)' + name_match = re.search(name_pattern, text) + extracted_name = name_match.group(1) if name_match else None + + # Common patterns for addresses with apartment/flat information + address_patterns = [ + # Pattern for addresses with flat/apartment info and city + r'(?:residence|address|live at|located at)[,\s]+(\d+\s+[\w\s]+(?:Street|St|Avenue|Ave|Road|Rd|Boulevard|Blvd|Lane|Ln|Drive|Dr|Court|Ct|Place|Pl|Way)[,\s]+(?:flat|apartment|apt|suite|ste|unit|#)\s+[\w\d]+)[,\s]+([\w\s]+)', + + # Simpler pattern as fallback + r'(\d+\s+[\w\s]+(?:Street|St|Avenue|Ave|Road|Rd|Boulevard|Blvd|Lane|Ln|Drive|Dr|Court|Ct|Place|Pl|Way)[,\s]+(?:flat|apartment|apt|suite|ste|unit|#)\s+[\w\d]+)[,\s]+([\w\s]+)' + ] + + street_address = None + city = None + + for pattern in address_patterns: + matches = re.search(pattern, text, re.IGNORECASE) + if matches: + street_address = matches.group(1).strip() + city = matches.group(2).strip() + break + + # Use a more specific prompt for the LLM + instruction = ( + "Extract contact fields from the transcript. Return STRICT JSON with keys: name, phone, street, city, state, zip, country.\n\n" + "VERY IMPORTANT INSTRUCTIONS:\n" + "1. For name extraction, look for full names like 'Sofia Martinez' or 'John Smith'. " + " Extract both first name and last name. The name should NEVER be null if a name is mentioned.\n\n" + "2. For address extraction:\n" + " - The 'street' field should include ONLY the street address with building number and apartment/flat info\n" + " - Do NOT include the city name in the street field\n" + " - Put the city name ONLY in the city field\n\n" + "Example 1: 'Good morning, this is Sofia Martinez here. Call me on 555-123-4567. My residence, 89 Queen Street, flat 7C, London.'\n" + "Correct extraction:\n" + "{\n" + " \"name\": \"Sofia Martinez\",\n" + " \"phone\": \"+5551234567\",\n" + " \"street\": \"89 Queen Street, flat 7C\",\n" + " \"city\": \"London\",\n" + " \"state\": null,\n" + " \"zip\": null,\n" + " \"country\": null\n" + "}\n\n" + "Example 2: 'Hello, I'm Alice Johnson. You can reach me at 987-654-3210. I live at 56 Park Avenue Suite 12, Boston.'\n" + "Correct extraction:\n" + "{\n" + " \"name\": \"Alice Johnson\",\n" + " \"phone\": \"+9876543210\",\n" + " \"street\": \"56 Park Avenue Suite 12\",\n" + " \"city\": \"Boston\",\n" + " \"state\": null,\n" + " \"zip\": null,\n" + " \"country\": null\n" + "}\n" + ) + prompt = f"{instruction}\n\nTranscript:\n{text}\n\nExtract only these fields and return as JSON." + + try: + client = ollama.Client(host=ollama_host) + resp = client.generate(model=ollama_model_name, prompt=prompt) + raw = resp.get("response", "") + parsed = _extract_first_json_block(raw) + if not parsed: + # Fallback if _extract_first_json_block fails + parsed = json.loads(raw.strip()) + + if not isinstance(parsed, dict): + raise ValueError("LLM returned non-dict JSON") + + def get_str(key): + val = parsed.get(key) + return val.strip() if isinstance(val, str) and val.strip() else None + + name = get_str("name") + phone = get_str("phone") + llm_street = get_str("street") + llm_city = get_str("city") + state = get_str("state") + zip_code = get_str("zip") + country = get_str("country") + + # Post-processing to fix common issues + + # 1. Fix name if it's null but we found one with regex + if not name and extracted_name: + name = extracted_name + + # 2. Try to extract name directly if still null + if not name: + # Look for common name patterns in the text + name_patterns = [ + r'((?:[A-Z][a-z]+ ){1,2}[A-Z][a-z]+) (?:here|speaking)', + r'(?:this is|my name is|I am|I\'m) ([A-Z][a-z]+ [A-Z][a-z]+)', + r'(?:name|caller):? ([A-Z][a-z]+ [A-Z][a-z]+)' + ] + + for pattern in name_patterns: + name_match = re.search(pattern, text, re.IGNORECASE) + if name_match: + name = name_match.group(1).strip() + break + + # 3. Clean up street address - remove city name from street if it appears there + if llm_street and llm_city and llm_city in llm_street: + # Remove the city and any trailing commas/spaces + llm_street = re.sub(r',?\s*' + re.escape(llm_city) + r'(?:,|\s|$)', '', llm_street).strip().rstrip(',') + + # Use LLM values with fallbacks + final_street = llm_street or street_address + final_city = llm_city or city + + # Final normalization and cleaning for phone + if phone: + phone = re.sub(r"[^\d+]", "", phone) + if phone.startswith("00"): + phone = "+" + phone[2:] + if phone and phone[0] != "+" and len(phone) >= 10: + phone = "+" + phone + + # Create full address string, ensuring no duplicates + address_parts = [] + if final_street: + address_parts.append(final_street) + if final_city and final_city not in final_street: + address_parts.append(final_city) + if state: + address_parts.append(state) + if zip_code: + address_parts.append(zip_code) + if country: + address_parts.append(country) + + address = ", ".join(address_parts) if address_parts else None + + result = { + "name": name, + "phone": phone, + "address": address, + "street": final_street, + "city": final_city, + "state": state, + "zip": zip_code, + "country": country, + } + + # Debug log the extraction + logger.info(f"Address extraction results - Input: '{text}', Extracted: {json.dumps(result, indent=2)}") + + return result + except Exception as e: + logger.warning(f"LLM detailed extraction failed: {e}") + # Return a basic extraction using regex patterns if LLM fails + return { + "name": extracted_name, + "phone": re.search(r'(\d{3}[-\.\s]?\d{3}[-\.\s]?\d{4})', text).group(1).replace('-', '') if re.search(r'(\d{3}[-\.\s]?\d{3}[-\.\s]?\d{4})', text) else None, + "address": f"{street_address}, {city}" if street_address and city else None, + "street": street_address, + "city": city, + "state": None, + "zip": None, + "country": None + } \ No newline at end of file From cbc84e8e29caaa4c9de9996fa44fe120abd7b972 Mon Sep 17 00:00:00 2001 From: arpita-josh02 Date: Sat, 6 Sep 2025 02:30:28 +0530 Subject: [PATCH 03/10] added lingo.main branch change in this --- service/config.py | 8 +++--- service/main.py | 66 +++++++++++++++++++++++++++++++++++++---------- 2 files changed, 57 insertions(+), 17 deletions(-) diff --git a/service/config.py b/service/config.py index f871898..f478864 100644 --- a/service/config.py +++ b/service/config.py @@ -5,11 +5,13 @@ openai_api_key = os.getenv("OPENAI_API_KEY") #model_id = os.getenv('MODEL_ID', 'large-v3') -model_id = os.getenv('MODEL_ID') -model_path = os.getenv('MODEL_PATH') +model_id = os.getenv('MODEL_ID','small') +model_path = os.getenv('MODEL_PATH', './models') ollama_host = os.getenv("OLLAMA_HOST", "http://ollama:11434") ollama_model_name = os.getenv("OLLAMA_MODEL_NAME", "llama3.2") +open_ai_model_name = os.getenv("OPENAI_MODEL_NAME", "gpt-4") +open_ai_temperature = os.getenv("OPENAI_TEMPERATURE", 0.2) odoo_url = os.getenv("ODOO_URL") odoo_db = os.getenv("ODOO_DB") odoo_username = os.getenv("ODOO_USERNAME") -odoo_password = os.getenv("ODOO_PASSWORD") \ No newline at end of file +odoo_password = os.getenv("ODOO_PASSWORD") diff --git a/service/main.py b/service/main.py index 3514ca2..b30beee 100644 --- a/service/main.py +++ b/service/main.py @@ -1,23 +1,22 @@ -from fastapi import FastAPI, HTTPException +from fastapi import FastAPI, UploadFile, File, Form +from fastapi import FastAPI, UploadFile, File, Form from fastapi.responses import JSONResponse from logger import logger from dotenv import load_dotenv from starlette.middleware.cors import CORSMiddleware from audio_service import translate_with_whisper -from audio_service import translate_with_whisper_timestamped +from audio_service import translate_with_whisper_timestamped, translate_with_whisper_from_upload +from intent import find_intent_using_openai, find_intent_using_regex from summarizer import summarize_using_openai -from summarizer import summarize_using_ollama, extract_contact_detailed_using_ollama +from summarizer import summarize_using_ollama,extract_contact_detailed_using_ollama from pydantic import BaseModel import traceback from util import generate_timestamp_json from fastapi_versionizer.versionizer import Versionizer, api_version -from config import odoo_url, odoo_db, odoo_username, odoo_password -from crm_client import OdooCRMClient -import os -import requests import json +from crm_client import OdooCRMClient +from config import odoo_url, odoo_db, odoo_username, odoo_password -import os.path app = FastAPI() # Add CORS middleware to the application @@ -29,6 +28,34 @@ allow_headers=["*"], # Allows all headers ) +@app.get("/") +def root_route(): + return 'Hello, this is the root route for lingo ai server' + +class Body(BaseModel): + audio_file_link: str + +def generate_timestamp_json(translation, summary, detected_language=None): + """Generate the final JSON response with all required fields""" + return { + "message": "File processed successfully!", + "translation": translation.get("text", ""), + "summary": summary, + "segments": translation.get("segments", []), + "detected_language": detected_language or translation.get("detected_language", "unknown") + } + + +def generate_timestamp_json(translation, summary, detected_language=None): + """Generate the final JSON response with all required fields""" + return { + "message": "File processed successfully!", + "translation": translation.get("text", ""), + "summary": summary, + "segments": translation.get("segments", []), + "detected_language": detected_language or translation.get("detected_language", "unknown") + } + @app.get("/") def root_route(): return 'Hello, this is the root route for lingo ai server' @@ -40,17 +67,23 @@ class Body(BaseModel): @app.post("/upload-audio") async def upload_audio(body: Body): try: + if body.audio_file_link == "": return JSONResponse(status_code=400, content={"message":"Invalid file link"}) - # Remove file extension check since frontend handles this translation = translate_with_whisper_timestamped(body.audio_file_link) - detected_language = translation.get('detected_language', 'unknown') + + # Extract detected language + detected_language = translation.get("detected_language", "unknown") + logger.info("translation done") summary = summarize_using_ollama(translation["text"]) logger.info("summary done") - result = generate_timestamp_json(translation,summary,detected_language) + + # Pass the translation object and detected_language to generate_timestamp_json + result = generate_timestamp_json(translation, summary, detected_language) + contact_info = extract_contact_detailed_using_ollama(translation["text"]) if "text" in translation else {"name": None, "phone": None, "address": None} @@ -119,14 +152,19 @@ async def upload_audio(body: Body): if body.audio_file_link == "": return JSONResponse(status_code=400, content={"message":"Invalid file link"}) - # Remove file extension check since frontend handles this translation = translate_with_whisper_timestamped(body.audio_file_link) - detected_language = translation.get('detected_language', 'unknown') + + # Extract detected language + detected_language = translation.get("detected_language", "unknown") + logger.info("translation done") summary = summarize_using_ollama(translation["text"]) logger.info("summary done") - result = generate_timestamp_json(translation,summary,detected_language) + + # Pass the translation object and detected_language to generate_timestamp_json + result = generate_timestamp_json(translation, summary, detected_language) + contact_info = extract_contact_detailed_using_ollama(translation["text"]) if "text" in translation else {"name": None, "phone": None, "address": None} From da43fc3100694d6774509770fc7820c8711dbfd0 Mon Sep 17 00:00:00 2001 From: Vishwajeetsingh Desurkar Date: Sat, 6 Sep 2025 14:13:59 +0530 Subject: [PATCH 04/10] Add core banking mock endpoint and fixed imports --- service/core_banking_mock.py | 39 ++++++++++++++++++++++++++++ service/main.py | 49 ++++++++++++++++++------------------ 2 files changed, 63 insertions(+), 25 deletions(-) create mode 100644 service/core_banking_mock.py diff --git a/service/core_banking_mock.py b/service/core_banking_mock.py new file mode 100644 index 0000000..b423c73 --- /dev/null +++ b/service/core_banking_mock.py @@ -0,0 +1,39 @@ +from fastapi import APIRouter +from datetime import datetime, timedelta + +router = APIRouter(prefix="/bank/me", tags=["banking"]) + +# Mock DB +BALANCE = 12500.50 +CONTACTS = {"Ananya": "ananya@upi", "Rajiv": "rajiv@upi"} +TRANSACTIONS = [ + {"id": 1, "merchant": "Amazon", "amount": -1200, "date": "2025-08-01"}, + {"id": 2, "merchant": "Swiggy", "amount": -500, "date": "2025-08-05"}, + {"id": 3, "merchant": "Salary", "amount": 50000, "date": "2025-08-01"}, + {"id": 4, "merchant": "Swiggy", "amount": -700, "date": "2025-08-15"}, +] + +@router.get("/balance") +async def get_balance(): + return {"balance": BALANCE} + +@router.post("/pay") +async def pay_money(to: str, amount: float): + global BALANCE + if amount > BALANCE: + return {"status": "failed", "reason": "Insufficient balance"} + BALANCE -= amount + TRANSACTIONS.append({ + "id": len(TRANSACTIONS) + 1, + "merchant": to, + "amount": -amount, + "date": datetime.now().strftime("%Y-%m-%d") + }) + return {"status": "success", "to": to, "amount": amount, "balance": BALANCE} + +@router.get("/transactions") +async def search_txn(merchant: str = None, limit: int = 5): + results = TRANSACTIONS + if merchant: + results = [t for t in TRANSACTIONS if merchant.lower() in t["merchant"].lower()] + return {"transactions": results[-limit:]} diff --git a/service/main.py b/service/main.py index b30beee..0f30aec 100644 --- a/service/main.py +++ b/service/main.py @@ -1,21 +1,29 @@ -from fastapi import FastAPI, UploadFile, File, Form -from fastapi import FastAPI, UploadFile, File, Form -from fastapi.responses import JSONResponse -from logger import logger +import json +import traceback + from dotenv import load_dotenv +from fastapi import FastAPI, File, Form, UploadFile +from fastapi.responses import JSONResponse +from fastapi_versionizer.versionizer import Versionizer, api_version +from pydantic import BaseModel from starlette.middleware.cors import CORSMiddleware -from audio_service import translate_with_whisper -from audio_service import translate_with_whisper_timestamped, translate_with_whisper_from_upload + +from audio_service import ( + translate_with_whisper, + translate_with_whisper_from_upload, + translate_with_whisper_timestamped, +) +from config import odoo_db, odoo_password, odoo_url, odoo_username +from core_banking_mock import router as core_banking_mock_router +from crm_client import OdooCRMClient from intent import find_intent_using_openai, find_intent_using_regex -from summarizer import summarize_using_openai -from summarizer import summarize_using_ollama,extract_contact_detailed_using_ollama -from pydantic import BaseModel -import traceback +from logger import logger +from summarizer import ( + extract_contact_detailed_using_ollama, + summarize_using_ollama, + summarize_using_openai, +) from util import generate_timestamp_json -from fastapi_versionizer.versionizer import Versionizer, api_version -import json -from crm_client import OdooCRMClient -from config import odoo_url, odoo_db, odoo_username, odoo_password app = FastAPI() @@ -28,6 +36,8 @@ allow_headers=["*"], # Allows all headers ) +app.include_router(core_banking_mock.router) + @app.get("/") def root_route(): return 'Hello, this is the root route for lingo ai server' @@ -35,17 +45,6 @@ def root_route(): class Body(BaseModel): audio_file_link: str -def generate_timestamp_json(translation, summary, detected_language=None): - """Generate the final JSON response with all required fields""" - return { - "message": "File processed successfully!", - "translation": translation.get("text", ""), - "summary": summary, - "segments": translation.get("segments", []), - "detected_language": detected_language or translation.get("detected_language", "unknown") - } - - def generate_timestamp_json(translation, summary, detected_language=None): """Generate the final JSON response with all required fields""" return { From d9cb5df785218656ef3347ceedc2b18d18b80941 Mon Sep 17 00:00:00 2001 From: Vishwajeetsingh Desurkar Date: Sat, 6 Sep 2025 14:21:59 +0530 Subject: [PATCH 05/10] Add missin imports and remove duplicate code --- service/main.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/service/main.py b/service/main.py index 0f30aec..daa208e 100644 --- a/service/main.py +++ b/service/main.py @@ -1,5 +1,7 @@ import json import traceback +import os +import requests from dotenv import load_dotenv from fastapi import FastAPI, File, Form, UploadFile @@ -55,12 +57,6 @@ def generate_timestamp_json(translation, summary, detected_language=None): "detected_language": detected_language or translation.get("detected_language", "unknown") } -@app.get("/") -def root_route(): - return 'Hello, this is the root route for lingo ai server' - -class Body(BaseModel): - audio_file_link: str # First API endpoint (v1) @api_version(1) @app.post("/upload-audio") @@ -172,7 +168,6 @@ async def upload_audio(body: Body): # Fire-and-forget CRM sync (do not block response) lead_id = None try: - print(odoo_url, odoo_db, odoo_username, odoo_password) if odoo_url and odoo_db and odoo_username and odoo_password: client = OdooCRMClient(odoo_url, odoo_db, odoo_username, odoo_password) lead_id = client.create_lead( From 1f296f5bdb35069c287a06a243d27dce9250b8bf Mon Sep 17 00:00:00 2001 From: Vishwajeetsingh Desurkar <32262119+Selectus2@users.noreply.github.com> Date: Sat, 6 Sep 2025 14:49:05 +0530 Subject: [PATCH 06/10] fixed core_banking_mock_router call Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- service/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/main.py b/service/main.py index daa208e..2af9fb9 100644 --- a/service/main.py +++ b/service/main.py @@ -38,7 +38,7 @@ allow_headers=["*"], # Allows all headers ) -app.include_router(core_banking_mock.router) +app.include_router(core_banking_mock_router) @app.get("/") def root_route(): From 178186c5b097bd9e2750e5225e271b763aa67964 Mon Sep 17 00:00:00 2001 From: naaz-josh Date: Mon, 8 Sep 2025 15:57:40 +0530 Subject: [PATCH 07/10] wip for crm --- app/src/app/crm/[id]/page.tsx | 66 ++++++ app/src/app/crm/page.tsx | 12 +- app/src/components/CRMItem.tsx | 311 +++++++++++++++++++++++++ app/src/components/DetailedCRM.tsx | 349 +++++++++++++++++++++++++++++ app/src/components/Navigation.tsx | 4 +- 5 files changed, 731 insertions(+), 11 deletions(-) create mode 100644 app/src/app/crm/[id]/page.tsx create mode 100644 app/src/components/CRMItem.tsx create mode 100644 app/src/components/DetailedCRM.tsx diff --git a/app/src/app/crm/[id]/page.tsx b/app/src/app/crm/[id]/page.tsx new file mode 100644 index 0000000..307dcde --- /dev/null +++ b/app/src/app/crm/[id]/page.tsx @@ -0,0 +1,66 @@ +import { Metadata } from "next"; +import DetailedCRM from "../../../components/DetailedCRM"; + +export const metadata: Metadata = { + title: "Lingo.ai | CRM Details", +}; + +interface PageProps { + params: Promise<{ + id: string; + }>; +} + +const page = async (props: PageProps) => { + const { id } = await props.params; + + // For now, we'll use static data. Later this will be replaced with actual API calls + const crmRecord = { + id: id, + name: "John Smith", + email: "john.smith@example.com", + phone: "+1 (555) 123-4567", + company: "Acme Corporation", + status: "Active", + lastContact: "2024-01-15", + location: "New York, NY", + leadSource: "Website", + recordingUrl: "https://www.soundjay.com/misc/sounds/bell-ringing-05.wav", + recordingName: "call_with_john_smith_jan15.mp3", + // CRM-specific data + translation: "This is a sample translation of the CRM call recording. The conversation covered product features, pricing, and next steps for the potential client.", + extraction: { + entities: [ + { type: "Person", value: "John Smith", confidence: 0.95 }, + { type: "Company", value: "Acme Corporation", confidence: 0.98 }, + { type: "Product", value: "Enterprise Software", confidence: 0.87 }, + { type: "Price", value: "$50,000", confidence: 0.92 }, + { type: "Date", value: "January 15, 2024", confidence: 0.99 }, + { type: "Location", value: "New York", confidence: 0.94 } + ], + keyPoints: [ + "Client interested in enterprise software solution", + "Budget range discussed: $40,000 - $60,000", + "Decision timeline: 2-3 weeks", + "Key stakeholders: John Smith, Sarah Johnson (CTO)", + "Next meeting scheduled for January 22nd" + ], + actionItems: [ + "Send detailed product specifications", + "Prepare custom pricing proposal", + "Schedule demo with technical team", + "Follow up on January 22nd" + ] + } + }; + + return ( +
+
+ +
+
+ ); +}; + +export default page; diff --git a/app/src/app/crm/page.tsx b/app/src/app/crm/page.tsx index 577ac70..f4257ac 100644 --- a/app/src/app/crm/page.tsx +++ b/app/src/app/crm/page.tsx @@ -1,4 +1,5 @@ import { Metadata } from "next"; +import CRMItem from "@/components/CRMItem"; export const metadata: Metadata = { title: "Lingo.ai | CRM", @@ -6,15 +7,8 @@ export const metadata: Metadata = { const CRMPage = () => { return ( -
-
-

CRM Dashboard

-
-

- CRM functionality will be implemented here. This is a placeholder page for the View CRM feature. -

-
-
+
+
); }; diff --git a/app/src/components/CRMItem.tsx b/app/src/components/CRMItem.tsx new file mode 100644 index 0000000..de223db --- /dev/null +++ b/app/src/components/CRMItem.tsx @@ -0,0 +1,311 @@ +"use client"; +import { useState, useEffect, useRef, useCallback } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"; +import { Database, User, Mail, Phone, Calendar, MapPin, Play, Pause, FileAudio } from "lucide-react"; +import { Table, TableBody, TableHead, TableHeader, TableRow } from "./ui/table"; +import { Badge } from "./ui/badge"; +import { Button } from "./ui/button"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "./ui/select"; +import Link from "next/link"; + +// Static CRM data for demonstration +const staticCRMData = [ + { + id: "1", + name: "John Smith", + email: "john.smith@example.com", + phone: "+1 (555) 123-4567", + company: "Acme Corporation", + status: "Active", + lastContact: "2024-01-15", + location: "New York, NY", + leadSource: "Website", + recordingUrl: "https://www.soundjay.com/misc/sounds/bell-ringing-05.wav", + recordingName: "call_with_john_smith_jan15.mp3", + }, + { + id: "2", + name: "Sarah Johnson", + email: "sarah.j@techcorp.com", + phone: "+1 (555) 987-6543", + company: "TechCorp Solutions", + status: "Prospect", + lastContact: "2024-01-12", + location: "San Francisco, CA", + leadSource: "Referral", + recordingUrl: "https://www.soundjay.com/misc/sounds/bell-ringing-05.wav", + recordingName: "meeting_sarah_johnson_jan12.mp3", + }, + { + id: "3", + name: "Michael Brown", + email: "m.brown@innovate.io", + phone: "+1 (555) 456-7890", + company: "Innovate Inc", + status: "Qualified", + lastContact: "2024-01-10", + location: "Austin, TX", + leadSource: "Cold Call", + recordingUrl: "https://www.soundjay.com/misc/sounds/bell-ringing-05.wav", + recordingName: "demo_call_michael_brown_jan10.mp3", + }, + { + id: "4", + name: "Emily Davis", + email: "emily.davis@startup.com", + phone: "+1 (555) 321-0987", + company: "StartupCo", + status: "Active", + lastContact: "2024-01-08", + location: "Seattle, WA", + leadSource: "Social Media", + recordingUrl: "https://www.soundjay.com/misc/sounds/bell-ringing-05.wav", + recordingName: "follow_up_emily_davis_jan08.mp3", + }, + { + id: "5", + name: "David Wilson", + email: "d.wilson@enterprise.com", + phone: "+1 (555) 654-3210", + company: "Enterprise Solutions", + status: "Closed", + lastContact: "2024-01-05", + location: "Chicago, IL", + leadSource: "Trade Show", + recordingUrl: "https://www.soundjay.com/misc/sounds/bell-ringing-05.wav", + recordingName: "closing_call_david_wilson_jan05.mp3", + }, +]; + +const CRMItem = () => { + const [statusFilter, setStatusFilter] = useState("all"); + + const handleFilterChange = (value: string) => { + setStatusFilter(value); + }; + + const filteredData = statusFilter === "all" + ? staticCRMData + : staticCRMData.filter(item => item.status.toLowerCase() === statusFilter.toLowerCase()); + + const getStatusBadgeVariant = (status: string) => { + switch (status.toLowerCase()) { + case "active": + return "default"; + case "prospect": + return "secondary"; + case "qualified": + return "outline"; + case "closed": + return "destructive"; + default: + return "secondary"; + } + }; + + return ( +
+
+
+ +
+ +
+

CRM Records

+

+ Manage and view your customer relationship management data +

+
+ + + + + + All CRM Records ({filteredData.length}) + + + + + + + + File Name + Name + Company + Contact + Status + Location + Lead Source + Last Contact + + + + {filteredData.map((record) => ( + + ))} + +
+
+
+
+
+ ); +}; + +interface CRMRecordRowProps { + record: typeof staticCRMData[0]; +} + +const CRMRecordRow = ({ record }: CRMRecordRowProps) => { + const [isPlaying, setIsPlaying] = useState(false); + const audioRef = useRef(null); + + const getStatusBadgeVariant = (status: string) => { + switch (status.toLowerCase()) { + case "active": + return "default"; + case "prospect": + return "secondary"; + case "qualified": + return "outline"; + case "closed": + return "destructive"; + default: + return "secondary"; + } + }; + + const handlePlayPause = () => { + if (isPlaying) { + audioRef.current?.pause(); + setIsPlaying(false); + } else { + audioRef.current?.play(); + setIsPlaying(true); + } + }; + + const handleAudioEnd = () => { + setIsPlaying(false); + }; + + useEffect(() => { + if (record.recordingUrl) { + const audio = new Audio(record.recordingUrl); + audioRef.current = audio; + + audio.addEventListener("ended", handleAudioEnd); + + return () => { + audio.removeEventListener("ended", handleAudioEnd); + audio.pause(); + }; + } + }, [record.recordingUrl]); + + useEffect(() => { + if (audioRef.current) { + if (isPlaying) { + audioRef.current.play(); + } else { + audioRef.current.pause(); + } + } + }, [isPlaying]); + + return ( + + + + + + +
+ + {record.recordingName} +
+ + + +
+ + {record.name} +
+ + +
+ {record.company} +
+ + +
+
+ + {record.email} +
+
+ + {record.phone} +
+
+ + +
+ + {record.status} + +
+ + +
+ + {record.location} +
+ + +
+ {record.leadSource} +
+ + +
+ + {record.lastContact} +
+ + + ); +}; + +export default CRMItem; diff --git a/app/src/components/DetailedCRM.tsx b/app/src/components/DetailedCRM.tsx new file mode 100644 index 0000000..adf79d0 --- /dev/null +++ b/app/src/components/DetailedCRM.tsx @@ -0,0 +1,349 @@ +"use client"; +import { format } from "date-fns"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { + FileAudio, + FileText, + Database, + PlayCircleIcon, + PauseCircleIcon, + User, + Mail, + Phone, + MapPin, + Calendar, + Building, +} from "lucide-react"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { cn } from "@/lib/utils"; +import Markdown from "react-markdown"; + +interface CRMRecord { + id: string; + name: string; + email: string; + phone: string; + company: string; + status: string; + lastContact: string; + location: string; + leadSource: string; + recordingUrl: string; + recordingName: string; + translation: string; + extraction: { + entities: Array<{ + type: string; + value: string; + confidence: number; + }>; + keyPoints: string[]; + actionItems: string[]; + }; +} + +const DetailedCRM = ({ crmRecord }: { crmRecord: CRMRecord }) => { + const [isPlaying, setIsPlaying] = useState(false); + const [audioDuration, setAudioDuration] = useState(null); + const audioRef = useRef(null); + const [currentTime, setCurrentTime] = useState(0); + + const getStatusBadgeVariant = (status: string) => { + switch (status.toLowerCase()) { + case "active": + return "default"; + case "prospect": + return "secondary"; + case "qualified": + return "outline"; + case "closed": + return "destructive"; + default: + return "secondary"; + } + }; + + const getProxyUrl = (audioUrl: string): string => { + if (audioUrl.includes('.s3.') || audioUrl.includes('s3.amazonaws.com')) { + return `/api/proxy?url=${encodeURIComponent(audioUrl)}`; + } + return audioUrl; + }; + + const handlePlayPause = useCallback(() => { + if (audioRef.current) { + if (isPlaying) { + audioRef.current.pause(); + setIsPlaying(false); + } else { + audioRef.current.play(); + setIsPlaying(true); + } + } + }, [isPlaying]); + + useEffect(() => { + if (crmRecord.recordingUrl) { + const proxyUrl = getProxyUrl(crmRecord.recordingUrl); + const audio = new Audio(proxyUrl); + audioRef.current = audio; + + const handleLoadedMetadata = () => { + setAudioDuration(formatDuration(audio.duration)); + }; + + const handleTimeUpdate = () => { + setCurrentTime(audio.currentTime); + }; + + const handleEnded = () => { + setIsPlaying(false); + setCurrentTime(0); + }; + + audio.addEventListener("loadedmetadata", handleLoadedMetadata); + audio.addEventListener("timeupdate", handleTimeUpdate); + audio.addEventListener("ended", handleEnded); + + return () => { + audio.removeEventListener("loadedmetadata", handleLoadedMetadata); + audio.removeEventListener("timeupdate", handleTimeUpdate); + audio.removeEventListener("ended", handleEnded); + audio.pause(); + }; + } + }, [crmRecord.recordingUrl]); + + const formatDuration = (seconds: number): string => { + const mins = Math.floor(seconds / 60); + const secs = Math.floor(seconds % 60); + return `${mins}:${secs.toString().padStart(2, "0")}`; + }; + + const formatTime = (time: number): string => { + const mins = Math.floor(time / 60); + const secs = Math.floor(time % 60); + return `${mins}:${secs.toString().padStart(2, "0")}`; + }; + + return ( +
+ {/* Header Section */} +
+

CRM Record Details

+

+ View and analyze customer relationship data and call recordings +

+
+ +
+ {/* CRM Info Section */} +
+ + + + + CRM Information + + + +
+ {/* Left Column - Contact Info */} +
+

Contact Information

+
+ + {crmRecord.name} +
+
+ + {crmRecord.company} +
+
+ + {crmRecord.email} +
+
+ + {crmRecord.phone} +
+
+ + {crmRecord.location} +
+
+ + {crmRecord.lastContact} +
+
+ + {/* Right Column - Status, Lead Source & Audio */} +
+
+

Status & Source

+
+
+ Status: + + {crmRecord.status} + +
+
+ Lead Source: + {crmRecord.leadSource} +
+
+
+ +
+

Recording

+
+ + {crmRecord.recordingName} +
+
+ Duration: {audioDuration || "Loading..."} +
+ +
+
+
+
+
+
+ + {/* Main Content Tabs */} +
+ + + + + Translation + + + + Extraction View + + +
+ {/* Translation Tab */} + + + + + + Call Translation + + + +
+ {crmRecord.translation} +
+
+
+
+ + {/* Extraction View Tab */} + +
+ {/* Entities */} + + + + + Extracted Entities + + + +
+ {crmRecord.extraction.entities.map((entity, index) => ( +
+
+ {entity.value} + + ({entity.type}) + +
+ + {Math.round(entity.confidence * 100)}% + +
+ ))} +
+
+
+ + {/* Key Points */} + + + + + Key Points + + + +
    + {crmRecord.extraction.keyPoints.map((point, index) => ( +
  • + + {point} +
  • + ))} +
+
+
+ + {/* Action Items */} + + + + + Action Items + + + +
    + {crmRecord.extraction.actionItems.map((item, index) => ( +
  • + + {item} +
  • + ))} +
+
+
+
+
+
+
+
+
+
+ ); +}; + +export default DetailedCRM; diff --git a/app/src/components/Navigation.tsx b/app/src/components/Navigation.tsx index ed47c54..ddab2e4 100644 --- a/app/src/components/Navigation.tsx +++ b/app/src/components/Navigation.tsx @@ -254,14 +254,14 @@ const Navigation = ({ isSignedIn }: NavigationProps) => { router.push("/transcriptions")} - className="cursor-pointer hover:!text-black hover:font-bold hover:!bg-[#668D7E]/30" + className="cursor-pointer hover:!text-white hover:font-bold hover:!bg-[#668D7E]" > Summarization router.push("/crm")} - className="cursor-pointer hover:!text-black hover:font-bold hover:!bg-[#668D7E]/30" + className="cursor-pointer hover:!text-white hover:font-bold hover:!bg-[#668D7E]" > View CRM From 6bb3075357e14f65e55a9ecc83b6ed952764d702 Mon Sep 17 00:00:00 2001 From: naaz-josh Date: Wed, 10 Sep 2025 13:19:57 +0530 Subject: [PATCH 08/10] Integerated Api for Listing and Details Page and made UI changes as per the response --- app/src/app/api/crm-leads/default/route.ts | 1 - app/src/app/api/crm-leads/route.ts | 21 +- .../app/api/crm-leads/simple/[id]/route.ts | 2 - app/src/app/api/transcribe/route.ts | 1 - app/src/app/api/transcribe/save/route.ts | 4 - app/src/app/api/transcriptions/route.ts | 1 - app/src/app/crm/[id]/page.tsx | 74 ++--- app/src/components/CRMItem.tsx | 274 +++++++++--------- app/src/components/DetailedCRM.tsx | 238 ++++----------- app/src/components/NavigateBack.tsx | 2 + app/src/constants/crm.ts | 22 ++ app/src/lib/crm-api.ts | 168 +++++++++++ 12 files changed, 416 insertions(+), 392 deletions(-) create mode 100644 app/src/constants/crm.ts create mode 100644 app/src/lib/crm-api.ts diff --git a/app/src/app/api/crm-leads/default/route.ts b/app/src/app/api/crm-leads/default/route.ts index 51a093a..668d5c3 100644 --- a/app/src/app/api/crm-leads/default/route.ts +++ b/app/src/app/api/crm-leads/default/route.ts @@ -17,7 +17,6 @@ export async function GET(req: Request) { data: defaultLeads }); } catch (error) { - console.error("Error fetching default CRM leads:", error); return NextResponse.json( { success: false, error: "Failed to fetch default CRM leads" }, { status: 500 } diff --git a/app/src/app/api/crm-leads/route.ts b/app/src/app/api/crm-leads/route.ts index 1ffcc15..aabec96 100644 --- a/app/src/app/api/crm-leads/route.ts +++ b/app/src/app/api/crm-leads/route.ts @@ -4,33 +4,26 @@ import { crmLeadsTable } from "@/db/schema"; export async function GET() { try { - console.log("Fetching all CRM leads"); + - // Query all CRM leads + const allLeads = await db .select() .from(crmLeadsTable); - console.log(`Found ${allLeads.length} CRM leads`); - if (allLeads.length > 0) { - console.log("Sample lead data:", { - id: allLeads[0].id, - leadId: allLeads[0].leadId, - fileName: allLeads[0].fileName - }); - } - - // Return the leads, even if empty + return NextResponse.json({ success: true, data: allLeads, count: allLeads.length }); } catch (error) { - console.error("Error retrieving CRM leads:", error); return NextResponse.json( - { success: false, error: String(error) }, + { + success: false, + error: error instanceof Error ? error.message : 'Unknown error' + }, { status: 500 } ); } diff --git a/app/src/app/api/crm-leads/simple/[id]/route.ts b/app/src/app/api/crm-leads/simple/[id]/route.ts index 349242d..d18fbac 100644 --- a/app/src/app/api/crm-leads/simple/[id]/route.ts +++ b/app/src/app/api/crm-leads/simple/[id]/route.ts @@ -10,7 +10,6 @@ export async function GET( try { const id = params.id; - console.log("Direct lookup for CRM lead with ID:", id); if (!id) { return NextResponse.json({ error: "ID is required" }, { status: 400 }); @@ -47,7 +46,6 @@ export async function GET( // Return the found lead return NextResponse.json(lead[0]); } catch (error) { - console.error("Error in direct CRM lead lookup:", error); return NextResponse.json({ error: String(error) }, { status: 500 }); } } \ No newline at end of file diff --git a/app/src/app/api/transcribe/route.ts b/app/src/app/api/transcribe/route.ts index 7a097c7..cfe6685 100644 --- a/app/src/app/api/transcribe/route.ts +++ b/app/src/app/api/transcribe/route.ts @@ -26,7 +26,6 @@ export async function POST(req: Request) { return new Response(JSON.stringify(data), { status: 200 }); } catch (error) { - console.log(error); if (error instanceof z.ZodError) { return new Response(error.message, { status: 422 }); } diff --git a/app/src/app/api/transcribe/save/route.ts b/app/src/app/api/transcribe/save/route.ts index e7c883f..7226854 100644 --- a/app/src/app/api/transcribe/save/route.ts +++ b/app/src/app/api/transcribe/save/route.ts @@ -50,7 +50,6 @@ export async function POST(req: Request) { // If lead ID exists, save CRM data if (leadId) { try { - console.log("Saving CRM lead data for lead ID:", leadId); await db.insert(crmLeadsTable).values({ leadId: leadId, @@ -64,16 +63,13 @@ export async function POST(req: Request) { isDefault: isDefault === true }); - console.log("CRM lead data saved successfully"); } catch (crmError) { - console.error("Error saving CRM lead data:", crmError); // Continue with the response even if CRM save fails } } return new Response(JSON.stringify(response), { status: 200 }); } catch (error) { - console.log(error); return new Response(JSON.stringify(error), { status: 500 }); } } \ No newline at end of file diff --git a/app/src/app/api/transcriptions/route.ts b/app/src/app/api/transcriptions/route.ts index 40a8a8e..889504e 100644 --- a/app/src/app/api/transcriptions/route.ts +++ b/app/src/app/api/transcriptions/route.ts @@ -61,7 +61,6 @@ export async function GET(req: NextRequest) { { status: 200 } ); } catch (error) { - console.log(error); return new Response(JSON.stringify(error), { status: 500 }); } } diff --git a/app/src/app/crm/[id]/page.tsx b/app/src/app/crm/[id]/page.tsx index 307dcde..a836f1c 100644 --- a/app/src/app/crm/[id]/page.tsx +++ b/app/src/app/crm/[id]/page.tsx @@ -1,5 +1,8 @@ import { Metadata } from "next"; import DetailedCRM from "../../../components/DetailedCRM"; +import { getCrmLeadById } from "@/lib/crm-api"; +import { notFound } from "next/navigation"; +import { CRM_CONSTANTS } from "@/constants/crm"; export const metadata: Metadata = { title: "Lingo.ai | CRM Details", @@ -13,54 +16,33 @@ interface PageProps { const page = async (props: PageProps) => { const { id } = await props.params; + + try { + const lead = await getCrmLeadById(id); + const extractedData = lead.extractedData as any || {}; + const crmRecord = { + id: lead.id, + leadId: lead.leadId, + crmUrl: lead.crmUrl, + fileName: lead.fileName, + contact: extractedData.contact || CRM_CONSTANTS.FALLBACK_DATA.CONTACT, + email: extractedData.email || '', + company: extractedData.company || CRM_CONSTANTS.FALLBACK_DATA.COMPANY, + lastContact: lead.createdAt ? new Date(lead.createdAt).toLocaleDateString() : '', + documentUrl: lead.documentUrl, + translation: lead.translation || CRM_CONSTANTS.FALLBACK_DATA.TRANSLATION + }; - // For now, we'll use static data. Later this will be replaced with actual API calls - const crmRecord = { - id: id, - name: "John Smith", - email: "john.smith@example.com", - phone: "+1 (555) 123-4567", - company: "Acme Corporation", - status: "Active", - lastContact: "2024-01-15", - location: "New York, NY", - leadSource: "Website", - recordingUrl: "https://www.soundjay.com/misc/sounds/bell-ringing-05.wav", - recordingName: "call_with_john_smith_jan15.mp3", - // CRM-specific data - translation: "This is a sample translation of the CRM call recording. The conversation covered product features, pricing, and next steps for the potential client.", - extraction: { - entities: [ - { type: "Person", value: "John Smith", confidence: 0.95 }, - { type: "Company", value: "Acme Corporation", confidence: 0.98 }, - { type: "Product", value: "Enterprise Software", confidence: 0.87 }, - { type: "Price", value: "$50,000", confidence: 0.92 }, - { type: "Date", value: "January 15, 2024", confidence: 0.99 }, - { type: "Location", value: "New York", confidence: 0.94 } - ], - keyPoints: [ - "Client interested in enterprise software solution", - "Budget range discussed: $40,000 - $60,000", - "Decision timeline: 2-3 weeks", - "Key stakeholders: John Smith, Sarah Johnson (CTO)", - "Next meeting scheduled for January 22nd" - ], - actionItems: [ - "Send detailed product specifications", - "Prepare custom pricing proposal", - "Schedule demo with technical team", - "Follow up on January 22nd" - ] - } - }; - - return ( -
-
- + return ( +
+
+ +
-
- ); + ); + } catch (error) { + notFound(); + } }; export default page; diff --git a/app/src/components/CRMItem.tsx b/app/src/components/CRMItem.tsx index de223db..93b2f4e 100644 --- a/app/src/components/CRMItem.tsx +++ b/app/src/components/CRMItem.tsx @@ -1,5 +1,5 @@ "use client"; -import { useState, useEffect, useRef, useCallback } from "react"; +import { useState, useEffect, useRef, useCallback, useMemo, memo } from "react"; import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"; import { Database, User, Mail, Phone, Calendar, MapPin, Play, Pause, FileAudio } from "lucide-react"; import { Table, TableBody, TableHead, TableHeader, TableRow } from "./ui/table"; @@ -13,127 +13,120 @@ import { SelectValue, } from "./ui/select"; import Link from "next/link"; +import { getAllCrmLeads, transformCrmLead } from "@/lib/crm-api"; +import { CRM_CONSTANTS } from "@/constants/crm"; + + +interface CRMDisplayData { + id: string; + leadId: string; + crmUrl: string; + fileName: string; + email: string; + company: string; + contact: string; + lastContact: string; + documentUrl: string; + translation: string; +} + -// Static CRM data for demonstration -const staticCRMData = [ - { - id: "1", - name: "John Smith", - email: "john.smith@example.com", - phone: "+1 (555) 123-4567", - company: "Acme Corporation", - status: "Active", - lastContact: "2024-01-15", - location: "New York, NY", - leadSource: "Website", - recordingUrl: "https://www.soundjay.com/misc/sounds/bell-ringing-05.wav", - recordingName: "call_with_john_smith_jan15.mp3", - }, - { - id: "2", - name: "Sarah Johnson", - email: "sarah.j@techcorp.com", - phone: "+1 (555) 987-6543", - company: "TechCorp Solutions", - status: "Prospect", - lastContact: "2024-01-12", - location: "San Francisco, CA", - leadSource: "Referral", - recordingUrl: "https://www.soundjay.com/misc/sounds/bell-ringing-05.wav", - recordingName: "meeting_sarah_johnson_jan12.mp3", - }, - { - id: "3", - name: "Michael Brown", - email: "m.brown@innovate.io", - phone: "+1 (555) 456-7890", - company: "Innovate Inc", - status: "Qualified", - lastContact: "2024-01-10", - location: "Austin, TX", - leadSource: "Cold Call", - recordingUrl: "https://www.soundjay.com/misc/sounds/bell-ringing-05.wav", - recordingName: "demo_call_michael_brown_jan10.mp3", - }, - { - id: "4", - name: "Emily Davis", - email: "emily.davis@startup.com", - phone: "+1 (555) 321-0987", - company: "StartupCo", - status: "Active", - lastContact: "2024-01-08", - location: "Seattle, WA", - leadSource: "Social Media", - recordingUrl: "https://www.soundjay.com/misc/sounds/bell-ringing-05.wav", - recordingName: "follow_up_emily_davis_jan08.mp3", - }, - { - id: "5", - name: "David Wilson", - email: "d.wilson@enterprise.com", - phone: "+1 (555) 654-3210", - company: "Enterprise Solutions", - status: "Closed", - lastContact: "2024-01-05", - location: "Chicago, IL", - leadSource: "Trade Show", - recordingUrl: "https://www.soundjay.com/misc/sounds/bell-ringing-05.wav", - recordingName: "closing_call_david_wilson_jan05.mp3", - }, -]; const CRMItem = () => { - const [statusFilter, setStatusFilter] = useState("all"); + const [crmData, setCrmData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); - const handleFilterChange = (value: string) => { - setStatusFilter(value); - }; + const transformLead = useCallback((lead: any): CRMDisplayData => { + const extractedData = lead.extractedData as any || {}; + return { + id: lead.id, + leadId: lead.leadId, + crmUrl: lead.crmUrl, + fileName: lead.fileName, + email: extractedData.email || '', + company: extractedData.company || CRM_CONSTANTS.FALLBACK_DATA.COMPANY, + contact: extractedData.contact || CRM_CONSTANTS.FALLBACK_DATA.CONTACT, + lastContact: lead.createdAt ? new Date(lead.createdAt).toLocaleDateString() : '', + documentUrl: lead.documentUrl, + translation: lead.translation + }; + }, []); - const filteredData = statusFilter === "all" - ? staticCRMData - : staticCRMData.filter(item => item.status.toLowerCase() === statusFilter.toLowerCase()); + useEffect(() => { + const fetchCrmData = async () => { + try { + setLoading(true); + setError(null); + const leads = await getAllCrmLeads(); + const transformedLeads = leads.map(transformLead); + setCrmData(transformedLeads); + } catch (err) { + setError(err instanceof Error ? err.message : CRM_CONSTANTS.MESSAGES.FAILED_TO_FETCH); + setCrmData([]); + } finally { + setLoading(false); + } + }; - const getStatusBadgeVariant = (status: string) => { - switch (status.toLowerCase()) { - case "active": - return "default"; - case "prospect": - return "secondary"; - case "qualified": - return "outline"; - case "closed": - return "destructive"; - default: - return "secondary"; - } - }; + fetchCrmData(); + }, [transformLead]); + + const filteredData = useMemo(() => crmData, [crmData]); + + if (loading) { + return ( +
+
+
+
+ {CRM_CONSTANTS.MESSAGES.LOADING} +
+
+
+ ); + } + + if (crmData.length === 0 && !error) { + return ( +
+
+

{CRM_CONSTANTS.UI.TITLE}

+

+ {CRM_CONSTANTS.UI.SUBTITLE} +

+
+ + + + +

{CRM_CONSTANTS.UI.EMPTY_STATE_TITLE}

+

+ {CRM_CONSTANTS.UI.EMPTY_STATE_DESCRIPTION} +

+
+
+
+ ); + } return (
-
- -
+ {error && ( +
+

+ Warning: {error}. Showing fallback data. +

+
+ )} +
-

CRM Records

+

{CRM_CONSTANTS.UI.TITLE}

- Manage and view your customer relationship management data + {CRM_CONSTANTS.UI.SUBTITLE} + {crmData.length > 0 && ` (${crmData.length} records)`}

@@ -149,13 +142,12 @@ const CRMItem = () => { + Lead ID File Name - Name - Company Contact - Status - Location - Lead Source + Company + Email + CRM URL Last Contact @@ -173,10 +165,10 @@ const CRMItem = () => { }; interface CRMRecordRowProps { - record: typeof staticCRMData[0]; + record: CRMDisplayData; } -const CRMRecordRow = ({ record }: CRMRecordRowProps) => { +const CRMRecordRow = memo(({ record }: CRMRecordRowProps) => { const [isPlaying, setIsPlaying] = useState(false); const audioRef = useRef(null); @@ -210,8 +202,8 @@ const CRMRecordRow = ({ record }: CRMRecordRowProps) => { }; useEffect(() => { - if (record.recordingUrl) { - const audio = new Audio(record.recordingUrl); + if (record.documentUrl) { + const audio = new Audio(record.documentUrl); audioRef.current = audio; audio.addEventListener("ended", handleAudioEnd); @@ -221,7 +213,7 @@ const CRMRecordRow = ({ record }: CRMRecordRowProps) => { audio.pause(); }; } - }, [record.recordingUrl]); + }, [record.documentUrl]); useEffect(() => { if (audioRef.current) { @@ -249,18 +241,26 @@ const CRMRecordRow = ({ record }: CRMRecordRowProps) => { )} + + +
+ + {record.leadId} +
+ +
- {record.recordingName} + {record.fileName}
- {record.name} + {record.contact}
@@ -269,33 +269,23 @@ const CRMRecordRow = ({ record }: CRMRecordRowProps) => {
-
+
{record.email}
-
- - {record.phone} -
-
- - -
- - {record.status} - -
- - -
- - {record.location}
- {record.leadSource} + + View in CRM +
@@ -306,6 +296,8 @@ const CRMRecordRow = ({ record }: CRMRecordRowProps) => { ); -}; +}); + +CRMRecordRow.displayName = 'CRMRecordRow'; export default CRMItem; diff --git a/app/src/components/DetailedCRM.tsx b/app/src/components/DetailedCRM.tsx index adf79d0..a06c12d 100644 --- a/app/src/components/DetailedCRM.tsx +++ b/app/src/components/DetailedCRM.tsx @@ -1,9 +1,6 @@ "use client"; -import { format } from "date-fns"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { FileAudio, FileText, @@ -12,60 +9,33 @@ import { PauseCircleIcon, User, Mail, - Phone, - MapPin, Calendar, Building, + ExternalLink, } from "lucide-react"; -import { useCallback, useEffect, useRef, useState } from "react"; -import { cn } from "@/lib/utils"; +import { useCallback, useEffect, useRef, useState, memo } from "react"; import Markdown from "react-markdown"; +import { CRM_CONSTANTS } from "@/constants/crm"; interface CRMRecord { id: string; - name: string; + leadId: string; + crmUrl: string; + fileName: string; + contact: string; email: string; - phone: string; company: string; - status: string; lastContact: string; - location: string; - leadSource: string; - recordingUrl: string; - recordingName: string; + documentUrl: string; translation: string; - extraction: { - entities: Array<{ - type: string; - value: string; - confidence: number; - }>; - keyPoints: string[]; - actionItems: string[]; - }; } -const DetailedCRM = ({ crmRecord }: { crmRecord: CRMRecord }) => { +const DetailedCRM = memo(({ crmRecord }: { crmRecord: CRMRecord }) => { const [isPlaying, setIsPlaying] = useState(false); const [audioDuration, setAudioDuration] = useState(null); const audioRef = useRef(null); const [currentTime, setCurrentTime] = useState(0); - const getStatusBadgeVariant = (status: string) => { - switch (status.toLowerCase()) { - case "active": - return "default"; - case "prospect": - return "secondary"; - case "qualified": - return "outline"; - case "closed": - return "destructive"; - default: - return "secondary"; - } - }; - const getProxyUrl = (audioUrl: string): string => { if (audioUrl.includes('.s3.') || audioUrl.includes('s3.amazonaws.com')) { return `/api/proxy?url=${encodeURIComponent(audioUrl)}`; @@ -86,8 +56,8 @@ const DetailedCRM = ({ crmRecord }: { crmRecord: CRMRecord }) => { }, [isPlaying]); useEffect(() => { - if (crmRecord.recordingUrl) { - const proxyUrl = getProxyUrl(crmRecord.recordingUrl); + if (crmRecord.documentUrl) { + const proxyUrl = getProxyUrl(crmRecord.documentUrl); const audio = new Audio(proxyUrl); audioRef.current = audio; @@ -115,7 +85,7 @@ const DetailedCRM = ({ crmRecord }: { crmRecord: CRMRecord }) => { audio.pause(); }; } - }, [crmRecord.recordingUrl]); + }, [crmRecord.documentUrl]); const formatDuration = (seconds: number): string => { const mins = Math.floor(seconds / 60); @@ -133,9 +103,9 @@ const DetailedCRM = ({ crmRecord }: { crmRecord: CRMRecord }) => {
{/* Header Section */}
-

CRM Record Details

+

{CRM_CONSTANTS.UI.DETAILS_TITLE}

- View and analyze customer relationship data and call recordings + {CRM_CONSTANTS.UI.DETAILS_SUBTITLE}

@@ -156,7 +126,7 @@ const DetailedCRM = ({ crmRecord }: { crmRecord: CRMRecord }) => {

Contact Information

- {crmRecord.name} + {crmRecord.contact}
@@ -166,34 +136,36 @@ const DetailedCRM = ({ crmRecord }: { crmRecord: CRMRecord }) => { {crmRecord.email}
-
- - {crmRecord.phone} -
-
- - {crmRecord.location} -
{crmRecord.lastContact}
- {/* Right Column - Status, Lead Source & Audio */} + {/* Right Column - Lead Info & Audio */}
-

Status & Source

-
-
- Status: - - {crmRecord.status} - +

Lead Information

+
+
+ Lead ID: + {crmRecord.leadId} +
+
+ File Name: + {crmRecord.fileName}
-
- Lead Source: - {crmRecord.leadSource} +
+ CRM URL: + + View in CRM + +
@@ -202,11 +174,9 @@ const DetailedCRM = ({ crmRecord }: { crmRecord: CRMRecord }) => {

Recording

- {crmRecord.recordingName} -
-
- Duration: {audioDuration || "Loading..."} + {crmRecord.fileName}
+
- {/* Main Content Tabs */} + {/* Translation Section */}
- - - - - Translation - - - - Extraction View - - -
- {/* Translation Tab */} - - - - - - Call Translation - - - -
- {crmRecord.translation} -
-
-
-
- - {/* Extraction View Tab */} - -
- {/* Entities */} - - - - - Extracted Entities - - - -
- {crmRecord.extraction.entities.map((entity, index) => ( -
-
- {entity.value} - - ({entity.type}) - -
- - {Math.round(entity.confidence * 100)}% - -
- ))} -
-
-
- - {/* Key Points */} - - - - - Key Points - - - -
    - {crmRecord.extraction.keyPoints.map((point, index) => ( -
  • - - {point} -
  • - ))} -
-
-
- - {/* Action Items */} - - - - - Action Items - - - -
    - {crmRecord.extraction.actionItems.map((item, index) => ( -
  • - - {item} -
  • - ))} -
-
-
-
-
-
-
+ + + + + Call Translation + + + +
+ {crmRecord.translation} +
+
+
); -}; +}); + +DetailedCRM.displayName = 'DetailedCRM'; -export default DetailedCRM; +export default DetailedCRM; \ No newline at end of file diff --git a/app/src/components/NavigateBack.tsx b/app/src/components/NavigateBack.tsx index 5d75099..9eb3d3e 100644 --- a/app/src/components/NavigateBack.tsx +++ b/app/src/components/NavigateBack.tsx @@ -15,6 +15,8 @@ const NavigateBack = (props: NavigateBackProps) => { const handleBack = () => { if (pathname.startsWith("/transcriptions/")) { router.push("/transcriptions"); + } else if (pathname.startsWith("/crm/")) { + router.push("/crm"); } else { href ? router.push(href) : router.back(); } diff --git a/app/src/constants/crm.ts b/app/src/constants/crm.ts new file mode 100644 index 0000000..8190a09 --- /dev/null +++ b/app/src/constants/crm.ts @@ -0,0 +1,22 @@ +export const CRM_CONSTANTS = { + MESSAGES: { + LOADING: "Loading CRM records...", + FAILED_TO_FETCH: "Failed to fetch CRM data", + UNKNOWN_COMPANY: "Unknown Company", + UNKNOWN_CONTACT: "Unknown", + NO_TRANSLATION: "No translation available", + }, + UI: { + TITLE: "CRM Records", + SUBTITLE: "Manage and view your customer relationship management data", + DETAILS_TITLE: "CRM Record Details", + DETAILS_SUBTITLE: "View and analyze customer relationship data and call recordings", + EMPTY_STATE_TITLE: "No CRM Records Found", + EMPTY_STATE_DESCRIPTION: "You don't have any CRM records yet. Start by uploading audio files or creating new leads.", + }, + FALLBACK_DATA: { + COMPANY: "Unknown Company", + CONTACT: "Unknown", + TRANSLATION: "No translation available", + } +} as const; diff --git a/app/src/lib/crm-api.ts b/app/src/lib/crm-api.ts new file mode 100644 index 0000000..621869e --- /dev/null +++ b/app/src/lib/crm-api.ts @@ -0,0 +1,168 @@ +import { CrmLeadsType } from "@/db/schema"; + + +interface ApiResponse { + success: boolean; + data?: T; + count?: number; + error?: string; +} + +interface CrmLeadWithDetails extends CrmLeadsType { + + name?: string; + email?: string; + phone?: string; + company?: string; + status?: string; + lastContact?: string; + location?: string; + leadSource?: string; + recordingUrl?: string; + recordingName?: string; + extraction?: { + entities: Array<{ + type: string; + value: string; + confidence: number; + }>; + keyPoints: string[]; + actionItems: string[]; + }; +} + + +export class CrmApiClient { + private baseUrl: string; + + constructor() { + this.baseUrl = process.env.NEXT_PUBLIC_API_URL || ''; + } + + private getApiUrl(endpoint: string): string { + if (this.baseUrl) { + return `${this.baseUrl}${endpoint}`; + } + // In server-side context, we need an absolute URL + if (typeof window === 'undefined') { + // Server-side: construct absolute URL + const baseUrl = process.env.NEXTAUTH_URL || process.env.VERCEL_URL || 'http://localhost:3000'; + return `${baseUrl}${endpoint}`; + } + + return endpoint; + } + + async getAllLeads(): Promise { + try { + const response = await fetch(this.getApiUrl('/api/crm-leads'), { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error(`Failed to fetch CRM leads: ${response.statusText}`); + } + + const result: ApiResponse = await response.json(); + + if (!result.success) { + throw new Error(result.error || 'Failed to fetch CRM leads'); + } + + return result.data || []; + } catch (error) { + throw error; + } + } + + + async getLeadById(id: string): Promise { + try { + const url = this.getApiUrl(`/api/crm-leads/simple/${id}`); + + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + const errorText = await response.text(); + + if (response.status === 404) { + throw new Error('CRM lead not found'); + } + throw new Error(`Failed to fetch CRM lead: ${response.statusText} - ${errorText}`); + } + + const result = await response.json(); + return result; + } catch (error) { + throw error; + } + } + + + async getDefaultLeads(): Promise { + try { + const response = await fetch(this.getApiUrl('/api/crm-leads/default'), { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error(`Failed to fetch default CRM leads: ${response.statusText}`); + } + + const result: ApiResponse = await response.json(); + + if (!result.success) { + throw new Error(result.error || 'Failed to fetch default CRM leads'); + } + + return result.data || []; + } catch (error) { + throw error; + } + } + + + transformLeadToDisplay(lead: CrmLeadsType): CrmLeadWithDetails { + + const extractedData = lead.extractedData as any || {}; + + return { + ...lead, + name: extractedData.name || extractedData.contactName || 'Unknown', + email: extractedData.email || extractedData.contactEmail || '', + phone: extractedData.phone || extractedData.contactPhone || '', + company: extractedData.company || extractedData.organization || 'Unknown Company', + status: extractedData.status || 'Active', + lastContact: lead.createdAt ? new Date(lead.createdAt).toISOString().split('T')[0] : '', + location: extractedData.location || extractedData.address || '', + leadSource: extractedData.leadSource || extractedData.source || 'Unknown', + recordingUrl: lead.documentUrl, + recordingName: lead.fileName, + extraction: { + entities: extractedData.entities || [], + keyPoints: extractedData.keyPoints || [], + actionItems: extractedData.actionItems || [] + } + }; + } +} + + +export const crmApiClient = new CrmApiClient(); + + +export const getAllCrmLeads = () => crmApiClient.getAllLeads(); +export const getCrmLeadById = (id: string) => crmApiClient.getLeadById(id); +export const getDefaultCrmLeads = () => crmApiClient.getDefaultLeads(); +export const transformCrmLead = (lead: CrmLeadsType) => crmApiClient.transformLeadToDisplay(lead); From 1e0bfd1b0cc38ea9965fb22ffec230dacfa2c866 Mon Sep 17 00:00:00 2001 From: naaz-josh Date: Wed, 10 Sep 2025 15:34:54 +0530 Subject: [PATCH 09/10] Removed any to actual type --- app/src/app/api/transcribe/save/route.ts | 6 ++- app/src/app/crm/[id]/page.tsx | 3 +- app/src/components/CRMItem.tsx | 20 ++------ app/src/components/DetailedCRM.tsx | 14 +----- app/src/lib/crm-api.ts | 5 +- app/src/types/crm.ts | 58 ++++++++++++++++++++++++ 6 files changed, 71 insertions(+), 35 deletions(-) create mode 100644 app/src/types/crm.ts diff --git a/app/src/app/api/transcribe/save/route.ts b/app/src/app/api/transcribe/save/route.ts index 7226854..51d1b6c 100644 --- a/app/src/app/api/transcribe/save/route.ts +++ b/app/src/app/api/transcribe/save/route.ts @@ -16,9 +16,11 @@ export async function POST(req: Request) { segments, detectedLanguage, isDefault, - } = body; // CRM data - const { leadId, crmUrl, extractedData } = body; + leadId, + crmUrl, + extractedData + } = body; diff --git a/app/src/app/crm/[id]/page.tsx b/app/src/app/crm/[id]/page.tsx index a836f1c..6f9fefb 100644 --- a/app/src/app/crm/[id]/page.tsx +++ b/app/src/app/crm/[id]/page.tsx @@ -3,6 +3,7 @@ import DetailedCRM from "../../../components/DetailedCRM"; import { getCrmLeadById } from "@/lib/crm-api"; import { notFound } from "next/navigation"; import { CRM_CONSTANTS } from "@/constants/crm"; +import { ExtractedData } from "@/types/crm"; export const metadata: Metadata = { title: "Lingo.ai | CRM Details", @@ -19,7 +20,7 @@ const page = async (props: PageProps) => { try { const lead = await getCrmLeadById(id); - const extractedData = lead.extractedData as any || {}; + const extractedData = (lead.extractedData as ExtractedData) || {}; const crmRecord = { id: lead.id, leadId: lead.leadId, diff --git a/app/src/components/CRMItem.tsx b/app/src/components/CRMItem.tsx index 93b2f4e..1c76f05 100644 --- a/app/src/components/CRMItem.tsx +++ b/app/src/components/CRMItem.tsx @@ -15,20 +15,8 @@ import { import Link from "next/link"; import { getAllCrmLeads, transformCrmLead } from "@/lib/crm-api"; import { CRM_CONSTANTS } from "@/constants/crm"; - - -interface CRMDisplayData { - id: string; - leadId: string; - crmUrl: string; - fileName: string; - email: string; - company: string; - contact: string; - lastContact: string; - documentUrl: string; - translation: string; -} +import { ExtractedData, CRMDisplayData } from "@/types/crm"; +import { CrmLeadsType } from "@/db/schema"; @@ -37,8 +25,8 @@ const CRMItem = () => { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const transformLead = useCallback((lead: any): CRMDisplayData => { - const extractedData = lead.extractedData as any || {}; + const transformLead = useCallback((lead: CrmLeadsType): CRMDisplayData => { + const extractedData = (lead.extractedData as ExtractedData) || {}; return { id: lead.id, leadId: lead.leadId, diff --git a/app/src/components/DetailedCRM.tsx b/app/src/components/DetailedCRM.tsx index a06c12d..7f28429 100644 --- a/app/src/components/DetailedCRM.tsx +++ b/app/src/components/DetailedCRM.tsx @@ -16,19 +16,7 @@ import { import { useCallback, useEffect, useRef, useState, memo } from "react"; import Markdown from "react-markdown"; import { CRM_CONSTANTS } from "@/constants/crm"; - -interface CRMRecord { - id: string; - leadId: string; - crmUrl: string; - fileName: string; - contact: string; - email: string; - company: string; - lastContact: string; - documentUrl: string; - translation: string; -} +import { CRMRecord } from "@/types/crm"; const DetailedCRM = memo(({ crmRecord }: { crmRecord: CRMRecord }) => { const [isPlaying, setIsPlaying] = useState(false); diff --git a/app/src/lib/crm-api.ts b/app/src/lib/crm-api.ts index 621869e..247d0f2 100644 --- a/app/src/lib/crm-api.ts +++ b/app/src/lib/crm-api.ts @@ -1,5 +1,5 @@ import { CrmLeadsType } from "@/db/schema"; - +import { ExtractedData } from "@/types/crm"; interface ApiResponse { success: boolean; @@ -134,8 +134,7 @@ export class CrmApiClient { transformLeadToDisplay(lead: CrmLeadsType): CrmLeadWithDetails { - - const extractedData = lead.extractedData as any || {}; + const extractedData = (lead.extractedData as ExtractedData) || {}; return { ...lead, diff --git a/app/src/types/crm.ts b/app/src/types/crm.ts new file mode 100644 index 0000000..8fa80ed --- /dev/null +++ b/app/src/types/crm.ts @@ -0,0 +1,58 @@ + +export interface ExtractedData { + + name?: string; + contactName?: string; + email?: string; + contactEmail?: string; + phone?: string; + contactPhone?: string; + contact?: string; + + company?: string; + organization?: string; + + status?: string; + leadSource?: string; + source?: string; + location?: string; + address?: string; + + + entities?: Array<{ + type: string; + value: string; + confidence: number; + }>; + keyPoints?: string[]; + actionItems?: string[]; + + + [key: string]: any; +} + +export interface CRMDisplayData { + id: string; + leadId: string; + crmUrl: string; + fileName: string; + email: string; + company: string; + contact: string; + lastContact: string; + documentUrl: string; + translation: string; +} + +export interface CRMRecord { + id: string; + leadId: string; + crmUrl: string; + fileName: string; + contact: string; + email: string; + company: string; + lastContact: string; + documentUrl: string; + translation: string; +} From 233d0cc37d0b58e84d7f4f0910d7722087e4654d Mon Sep 17 00:00:00 2001 From: Vishwajeetsingh Desurkar Date: Wed, 10 Sep 2025 15:46:44 +0530 Subject: [PATCH 10/10] Remove duplicate imports and duplicate method definations --- service/main.py | 5 ++--- service/summarizer.py | 3 +-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/service/main.py b/service/main.py index a5045d4..a68fa0c 100644 --- a/service/main.py +++ b/service/main.py @@ -112,7 +112,7 @@ async def upload_audio(body: Body): ) logger.info(f"CRM: Address update result={updated} for partner_id={partner_id}") - # Add CRM data to the result + # Add CRM data to the result result["leadId"] = str(lead_id) result["crmUrl"] = odoo_url result["extractedData"] = contact_info @@ -222,8 +222,6 @@ async def upload_audio(body: Body): latest_prefix='/latest', sort_routes=True ).versionize() -# Add this function to call our new API endpoint -async def save_crm_lead_data(lead_id, file_path, translation, extracted_data, summary, user_id=None, transcription_id=None): app.include_router(core_banking_mock_router) @@ -319,6 +317,7 @@ async def fetch_default_crm_leads(): except Exception as e: logger.error(f"Error in fetch_default_crm_leads: {str(e)}") return JSONResponse(content={"message": str(e)}, status_code=500) + async def save_crm_lead_data(lead_id, file_path, translation, extracted_data, summary, user_id=None, transcription_id=None): """ diff --git a/service/summarizer.py b/service/summarizer.py index 1347c7e..d1fd95b 100644 --- a/service/summarizer.py +++ b/service/summarizer.py @@ -7,8 +7,7 @@ import json import re from config import ollama_host, ollama_model_name -import re -import json + logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__)