├── .env ├── .env.local ├── .eslintrc.json ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── app ├── api │ ├── meetings │ │ ├── [id] │ │ │ ├── export │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ └── route.ts │ ├── monitored-files │ │ ├── [fileName] │ │ │ └── route.ts │ │ └── route.ts │ └── transcribe │ │ └── route.ts ├── dashboard │ ├── meeting │ │ └── [id] │ │ │ └── page.tsx │ └── page.tsx ├── favicon.ico ├── fonts │ ├── GeistMonoVF.woff │ └── GeistVF.woff ├── globals.css ├── layout.tsx └── page.tsx ├── components.json ├── components ├── CategoryCard.tsx ├── MeetingDetails.tsx ├── UploadAudio.tsx └── ui │ ├── button.tsx │ ├── card.tsx │ ├── dropdown-menu.tsx │ ├── input.tsx │ ├── label.tsx │ ├── scroll-area.tsx │ ├── table.tsx │ ├── tabs.tsx │ ├── toast.tsx │ └── toaster.tsx ├── compose.yaml ├── hooks └── use-toast.ts ├── lib ├── prisma.ts └── utils.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── postcss.config.mjs ├── prisma ├── dev.db ├── migrations │ ├── 20241009233612_init │ │ └── migration.sql │ ├── 20241010010041_init │ │ └── migration.sql │ ├── 20241010054943_init │ │ └── migration.sql │ └── migration_lock.toml └── schema.prisma ├── public ├── dashboard.png ├── landing.webp ├── meeting_details.png └── meeting_summary.png ├── tailwind.config.ts ├── tsconfig.json └── utils ├── demo └── MeetingMind.mp4 └── langflow_flow └── Meeting Mind.json /.env: -------------------------------------------------------------------------------- 1 | # Environment variables declared in this file are automatically made available to Prisma. 2 | # See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema 3 | 4 | # Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB. 5 | # See the documentation for all the connection string options: https://pris.ly/d/connection-strings 6 | # .env 7 | DATABASE_URL="file:./dev.db" 8 | # DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public" -------------------------------------------------------------------------------- /.env.local: -------------------------------------------------------------------------------- 1 | LANGFLOW_FLOW_URL="http://127.0.0.1:7860/api/v1/run/5781a690-e689-4b26-b636-45da76a91915" -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | # .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:latest 2 | 3 | WORKDIR /app 4 | COPY package.json . 5 | 6 | 7 | RUN npm install --legacy-peer-deps 8 | 9 | COPY . . 10 | 11 | RUN npx prisma generate 12 | RUN npx prisma migrate dev --name init 13 | 14 | EXPOSE 3000 15 | CMD npm run dev 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Misbah Syed 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Meeting Mind- An Hour Long Meeting Analyzed in Under 30 Seconds (Powered by Langflow) 2 | 3 | MeetingMind is an AI-powered meeting assistant that helps you capture, analyze, and act on your meeting insights effortlessly. This project is built with Langflow, Next.js and Groq-based fast transcription service to analyze your meetings and generate insights. 4 | 5 | 6 | ## Demo 7 | 8 | Check out this demo video to see MeetingMind in action: 9 | 10 | https://github.com/user-attachments/assets/50a9de7a-b24f-4167-9526-4e112b1d24f8 11 | 12 | 13 | 14 | ## Features 15 | 16 | - Audio recording and file upload 17 | - AI-powered transcription 18 | - Automatic extraction of key information: 19 | - Tasks 20 | - Decisions 21 | - Questions 22 | - Insights 23 | - Deadlines 24 | - Attendees 25 | - Follow-ups 26 | - Risks 27 | - Agenda 28 | 29 | ## Getting Started 30 | 31 | ### Prerequisites 32 | 33 | - Node.js 14.x or later 34 | - npm or yarn 35 | - A LangFlow server running locally 36 | - Git (for cloning the repository) 37 | 38 | ### Caution 39 | 40 | ⚠️ **Important:** Groq Whisper used for transcription and analysis, currently supports files up to 25 MB only. There is a compression step in the process to reduce the file size to a manageable level. If your audio file is still larger than 25 MB, you will need to compress it before uploading. This limitation may affect the processing of longer meetings or high-quality audio recordings. 41 | 42 | To compress your audio files further, you can use tools like: 43 | - Online audio compressors 44 | - FFmpeg (command-line tool for audio/video processing) 45 | 46 | Ensure your compressed audio maintains sufficient quality for accurate transcription while staying under the 25 MB limit. 47 | 48 | ### Installation 49 | 50 | 1. Clone the repository: 51 | ```bash 52 | git clone https://github.com/yourusername/meetingmind.git 53 | cd meetingmind 54 | ``` 55 | 56 | 2. Install dependencies: 57 | ```bash 58 | npm install 59 | # or 60 | yarn install 61 | ``` 62 | 63 | 3. Set up LangFlow: 64 | - Install and run the LangFlow backend server 65 | - Upload the flow provided in the repo at `utils/langflow_flow/Meeting Mind.json` 66 | - Note the URL of your LangFlow server 67 | 68 | 4. Create a `.env.local` file in the root directory and add the LangFlow URL: 69 | ``` 70 | LANGFLOW_FLOW_URL="http://127.0.0.1:7860/api/v1/run/5781a690-e689-4b26-b636-45da76a91915" 71 | ``` 72 | Replace the URL with your actual LangFlow server URL if different. 73 | 74 | In the file `app/api/transcribe/route.ts`, locate the `payload` object and update the Groq component name to match your LangFlow component name. For example: 75 | 76 | ```typescript 77 | const payload = { 78 | output_type: 'text', 79 | input_type: 'text', 80 | tweaks: { 81 | 'YourGroqComponentName': { 82 | audio_file: filePath 83 | }, 84 | } 85 | } 86 | ``` 87 | 88 | Replace 'YourGroqComponentName' with the actual name of your Groq component in LangFlow. 89 | 90 | 5. Set up the database: 91 | 92 | This project uses Prisma as an ORM. By default, it's configured to use SQLite as the database. 93 | 94 | a. To use the local SQLite database: 95 | - Ensure your `.env` file contains: 96 | ``` 97 | DATABASE_URL="file:./dev.db" 98 | ``` 99 | - Run the following commands to set up your database: 100 | ```bash 101 | npx prisma generate 102 | npx prisma migrate dev --name init 103 | ``` 104 | 105 | b. To use a different database (e.g., PostgreSQL with Neon): 106 | - Update your `.env` file with the appropriate connection string: 107 | ``` 108 | DATABASE_URL="postgresql://username:password@host:port/database?schema=public" 109 | ``` 110 | - Update the `provider` in `prisma/schema.prisma`: 111 | ```prisma 112 | datasource db { 113 | provider = "postgresql" 114 | url = env("DATABASE_URL") 115 | } 116 | ``` 117 | - Run the Prisma commands as mentioned above to generate the client and run migrations. 118 | 119 | 6. Run the development server: 120 | ```bash 121 | npm run dev 122 | # or 123 | yarn dev 124 | ``` 125 | 126 | 7. Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 127 | 128 | ## Docker Setup 129 | Docker compose setup will run three containers of Sqlite, Langflow, Postgres. Postgres is used to store the Langflow data. 130 | 131 | 1. Use the command to run the containers. 132 | ```bash 133 | docker compose up 134 | ``` 135 | 136 | 2. Langflow setup 137 | 138 | - Upload the flow provided in the repo at `utils/langflow_flow/Meeting Mind.json` 139 | - Note the URL of your LangFlow server 140 | - Paste the url on .env.local file 141 | 142 | 3. Build the application image 143 | ```bash 144 | docker build -t YOUR-IMAGE-NAME . 145 | ``` 146 | 4. Run the application 147 | ```bash 148 | docker run -p 3000:3000 YOUR-IMAGE-NAME 149 | ``` 150 | 151 | ## Usage 152 | 153 | 1. Navigate to the dashboard page. 154 | 2. Upload an audio file. 155 | 3. Wait for the AI to process and analyze the meeting. 156 | 4. Review the extracted information in the Dashboard. 157 | 158 | ## Project Structure 159 | 160 | - `app/`: Contains the main application code 161 | - `components/`: Reusable React components 162 | - `api/`: API routes for server-side functionality 163 | - `dashboard/`: Dashboard page component 164 | - `page.tsx`: Home page component 165 | - `public/`: Static assets 166 | - `prisma/`: Database schema and migrations 167 | - `utils/`: Utility functions and configurations 168 | - `lib/`: Shared libraries and modules 169 | 170 | ## Technologies Used 171 | 172 | - Langflow: For AI workflow management 173 | - Next.js: React framework for building the web application 174 | - React: JavaScript library for building user interfaces 175 | - Tailwind CSS: Utility-first CSS framework 176 | - Framer Motion: Animation library for React 177 | - Axios: Promise-based HTTP client 178 | - Prisma: ORM for database management 179 | - SQLite: Default database (can be changed to PostgreSQL or others) 180 | - Groq: AI model provider for transcription and analysis 181 | 182 | ## Configuration 183 | 184 | - The project uses environment variables for configuration. Ensure all necessary variables are set in your `.env.local` file. 185 | - Tailwind CSS configuration can be found in `tailwind.config.ts`. 186 | - TypeScript configuration is in `tsconfig.json`. 187 | 188 | ## API Routes 189 | 190 | - `/api/meetings`: Handles CRUD operations for meetings 191 | - `/api/transcribe`: Handles audio file transcription and analysis 192 | 193 | ## Debugging 194 | 195 | - Use the browser's developer tools to debug client-side issues. 196 | - For server-side debugging, use console.log statements or attach a debugger to your Node.js process. 197 | 198 | ## Performance Considerations 199 | 200 | - Large audio files may take longer to process. Consider implementing a progress indicator for better user experience. 201 | - Optimize database queries and indexes for improved performance as the number of meetings grows. 202 | 203 | ## Screenshots 204 | 205 | ### Landing Page 206 | ![Landing Page](public/landing.webp) 207 | 208 | ### Dashboard 209 | ![Dashboard](public/dashboard.png) 210 | 211 | #### Meeting Summary 212 | ![Meeting Summary](public/meeting_summary.png) 213 | 214 | #### Meeting Details 215 | ![Meeting Details](public/meeting_details.png) 216 | 217 | These screenshots provide a visual representation of the application's main interfaces. The landing page showcases the initial user experience, while the dashboard displays the core functionality where users can upload audio files and view the AI-processed meeting information. 218 | 219 | ## Contributing 220 | 221 | Contributions are welcome! Please feel free to submit a Pull Request. Here are some ways you can contribute: 222 | 223 | - Report bugs and issues 224 | - Suggest new features 225 | - Improve documentation 226 | - Submit pull requests with bug fixes or new features 227 | 228 | Please read our contributing guidelines before submitting a pull request. 229 | 230 | ## License 231 | 232 | This project is licensed under the MIT License. See the LICENSE file for details. 233 | 234 | ## Support 235 | 236 | If you encounter any problems or have questions, please open an issue on the GitHub repository. 237 | 238 | ## Acknowledgements 239 | 240 | - Thanks to the Langflow team for providing the AI workflow management tool. 241 | - Special thanks to all contributors who have helped shape this project. 242 | -------------------------------------------------------------------------------- /app/api/meetings/[id]/export/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server' 2 | import { prisma } from '@/lib/prisma' 3 | import { Document, Packer, Paragraph, TextRun } from 'docx' 4 | 5 | export const GET = async (request: Request, { params }: { params: { id: string } }) => { 6 | const { id } = params 7 | 8 | // Handle CORS 9 | if (request.method === 'OPTIONS') { 10 | return new Response(null, { 11 | headers: { 12 | 'Access-Control-Allow-Origin': '*', // Or specific origin 13 | 'Access-Control-Allow-Methods': 'GET,OPTIONS', 14 | 'Access-Control-Allow-Headers': 'Content-Type', 15 | }, 16 | }) 17 | } 18 | 19 | try { 20 | const meeting = await prisma.meeting.findUnique({ 21 | where: { id }, 22 | include: { 23 | tasks: true, 24 | decisions: true, 25 | questions: true, 26 | insights: true, 27 | deadlines: true, 28 | attendees: true, 29 | followUps: true, 30 | risks: true, 31 | agenda: true, 32 | }, 33 | }) 34 | 35 | if (!meeting) { 36 | return NextResponse.json({ error: 'Meeting not found.' }, { status: 404 }) 37 | } 38 | 39 | // Create the document 40 | const doc = new Document({ 41 | sections: [ 42 | { 43 | properties: {}, 44 | children: [ 45 | new Paragraph({ 46 | text: meeting.name, 47 | heading: 'Heading1', 48 | }), 49 | new Paragraph({ 50 | text: 'Description', 51 | heading: 'Heading2', 52 | }), 53 | new Paragraph(meeting.description), 54 | new Paragraph({ 55 | text: 'Summary', 56 | heading: 'Heading2', 57 | }), 58 | new Paragraph(meeting.summary), 59 | new Paragraph({ 60 | text: 'Transcript', 61 | heading: 'Heading2', 62 | }), 63 | new Paragraph(meeting.rawTranscript), 64 | ...formatSection('Tasks', meeting.tasks.map((task: { task: any; owner: any; dueDate: any }) => `**Task:** ${task.task}\n**Owner:** ${task.owner}\n**Due Date:** ${task.dueDate}`)), 65 | ...formatSection('Decisions', meeting.decisions.map((decision: { decision: any; date: any }) => `**Decision:** ${decision.decision}\n**Date:** ${decision.date}`)), 66 | ...formatSection('Questions', meeting.questions.map((question: { question: any; status: any; answer: any }) => `**Question:** ${question.question}\n**Status:** ${question.status}\n**Answer:** ${question.answer || 'N/A'}`)), 67 | ...formatSection('Insights', meeting.insights.map((insight: { insight: any; reference: any }) => `${insight.insight} (Reference: ${insight.reference})`)), 68 | ...formatSection('Deadlines', meeting.deadlines.map((deadline: { description: any; dueDate: any }) => `**Description:** ${deadline.description}\n**Due Date:** ${deadline.dueDate}`)), 69 | ...formatSection('Attendees', meeting.attendees.map((attendee: { name: any; role: any }) => `${attendee.name} (${attendee.role})`)), 70 | ...formatSection('Follow-ups', meeting.followUps.map((followUp: { description: any; owner: any }) => `**Follow-up:** ${followUp.description}\n**Owner:** ${followUp.owner}`)), 71 | ...formatSection('Risks', meeting.risks.map((risk: { risk: any; impact: any }) => `**Risk:** ${risk.risk}\n**Impact:** ${risk.impact}`)), 72 | ...formatSection('Agenda', meeting.agenda.map((item: { item: any }) => `${item.item}`)), 73 | ], 74 | }, 75 | ], 76 | }) 77 | 78 | const buffer = await Packer.toBuffer(doc) 79 | const fileName = `${meeting.name.replace(/\s+/g, '_')}_Details.docx` 80 | 81 | return new NextResponse(buffer, { 82 | status: 200, 83 | headers: { 84 | 'Content-Type': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 85 | 'Content-Disposition': `attachment; filename="${fileName}"`, 86 | }, 87 | }) 88 | } catch (error: any) { 89 | console.error(error) 90 | return NextResponse.json({ error: 'Failed to export meeting details.' }, { status: 500 }) 91 | } 92 | } 93 | 94 | const formatSection = (title: string, items: string[]) => { 95 | const paragraphs = [ 96 | new Paragraph({ 97 | text: title, 98 | heading: 'Heading2', 99 | spacing: { before: 300, after: 200 }, 100 | }), 101 | ] 102 | 103 | items.forEach(item => { 104 | paragraphs.push( 105 | new Paragraph({ 106 | children: [ 107 | new TextRun({ 108 | text: item, 109 | break: 1, 110 | }), 111 | ], 112 | spacing: { after: 100 }, 113 | }) 114 | ) 115 | }) 116 | 117 | return paragraphs 118 | } 119 | -------------------------------------------------------------------------------- /app/api/meetings/[id]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server' 2 | import { prisma } from '@/lib/prisma' 3 | 4 | export const GET = async (request: NextRequest, { params }: { params: { id: string } }) => { 5 | const { id } = params 6 | 7 | try { 8 | const meeting = await prisma.meeting.findUnique({ 9 | where: { id }, 10 | select: { 11 | id: true, 12 | name: true, 13 | description: true, 14 | rawTranscript: true, 15 | summary: true, 16 | createdAt: true, 17 | updatedAt: true, 18 | tasks: { 19 | select: { 20 | id: true, 21 | task: true, 22 | owner: true, 23 | dueDate: true, 24 | }, 25 | }, 26 | decisions: { 27 | select: { 28 | id: true, 29 | decision: true, 30 | date: true, 31 | }, 32 | }, 33 | questions: { 34 | select: { 35 | id: true, 36 | question: true, 37 | status: true, 38 | answer: true, 39 | }, 40 | }, 41 | insights: { 42 | select: { 43 | id: true, 44 | insight: true, 45 | reference: true, 46 | }, 47 | }, 48 | deadlines: { 49 | select: { 50 | id: true, 51 | description: true, 52 | dueDate: true, 53 | }, 54 | }, 55 | attendees: { 56 | select: { 57 | id: true, 58 | name: true, 59 | role: true, 60 | }, 61 | }, 62 | followUps: { 63 | select: { 64 | id: true, 65 | description: true, 66 | owner: true, 67 | }, 68 | }, 69 | risks: { 70 | select: { 71 | id: true, 72 | risk: true, 73 | impact: true, 74 | }, 75 | }, 76 | agenda: { 77 | select: { 78 | id: true, 79 | item: true, 80 | }, 81 | }, 82 | }, 83 | }) 84 | 85 | if (!meeting) { 86 | return NextResponse.json({ error: 'Meeting not found.' }, { status: 404 }) 87 | } 88 | 89 | const formattedMeeting = { 90 | name: meeting.name, 91 | description: meeting.description, 92 | transcript: meeting.rawTranscript, 93 | summary: meeting.summary, 94 | breakdown: { 95 | Tasks: meeting.tasks.map((task: { task: any; owner: any; dueDate: any }) => ({ task: task.task, owner: task.owner, due_date: task.dueDate })), 96 | Decisions: meeting.decisions.map((decision: { decision: any; date: any }) => ({ decision: decision.decision, date: decision.date })), 97 | Questions: meeting.questions.map((question: { question: any; status: any; answer: any }) => ({ question: question.question, status: question.status, answer: question.answer })), 98 | Insights: meeting.insights.map((insight: { insight: any; reference: any }) => ({ insight: insight.insight, reference: insight.reference })), 99 | Deadlines: meeting.deadlines.map((deadline: { description: any; dueDate: any }) => ({ description: deadline.description, due_date: deadline.dueDate })), 100 | Attendees: meeting.attendees.map((attendee: { name: any; role: any }) => ({ name: attendee.name, role: attendee.role })), 101 | "Follow-ups": meeting.followUps.map((followUp: { description: any; owner: any }) => ({ description: followUp.description, owner: followUp.owner })), 102 | Risks: meeting.risks.map((risk: { risk: any; impact: any }) => ({ risk: risk.risk, impact: risk.impact })), 103 | Agenda: meeting.agenda.map((item: { item: any }) => ({ item: item.item })), 104 | }, 105 | } 106 | console.log(formattedMeeting) 107 | return NextResponse.json(formattedMeeting, { status: 200 }) 108 | } catch (error: any) { 109 | console.error(error) 110 | return NextResponse.json({ error: 'Failed to fetch meeting details.' }, { status: 500 }) 111 | } 112 | } 113 | 114 | export async function DELETE( 115 | request: NextRequest, 116 | { params }: { params: { id: string } } 117 | ) { 118 | const { id } = params 119 | 120 | try { 121 | // Delete all associated records 122 | await prisma.$transaction([ 123 | prisma.task.deleteMany({ where: { meetingId: id } }), 124 | prisma.decision.deleteMany({ where: { meetingId: id } }), 125 | prisma.question.deleteMany({ where: { meetingId: id } }), 126 | prisma.insight.deleteMany({ where: { meetingId: id } }), 127 | prisma.deadline.deleteMany({ where: { meetingId: id } }), 128 | prisma.attendee.deleteMany({ where: { meetingId: id } }), 129 | prisma.followUp.deleteMany({ where: { meetingId: id } }), 130 | prisma.risk.deleteMany({ where: { meetingId: id } }), 131 | prisma.agendaItem.deleteMany({ where: { meetingId: id } }), 132 | prisma.meeting.delete({ where: { id } }), 133 | ]) 134 | 135 | return NextResponse.json({ message: 'Meeting deleted successfully' }, { status: 200 }) 136 | } catch (error) { 137 | console.error('Error deleting meeting:', error) 138 | return NextResponse.json({ error: 'Failed to delete meeting' }, { status: 500 }) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /app/api/meetings/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server' 2 | import { prisma } from '@/lib/prisma' 3 | 4 | export const GET = async () => { 5 | try { 6 | const meetings = await prisma.meeting.findMany({ 7 | select: { 8 | id: true, 9 | name: true, 10 | description: true, 11 | createdAt: true, 12 | updatedAt: true, 13 | rawTranscript: true, 14 | summary: true, 15 | tasks: { 16 | select: { 17 | id: true, 18 | task: true, 19 | owner: true, 20 | dueDate: true, 21 | }, 22 | }, 23 | decisions: { 24 | select: { 25 | id: true, 26 | decision: true, 27 | date: true, 28 | }, 29 | }, 30 | questions: { 31 | select: { 32 | id: true, 33 | question: true, 34 | status: true, 35 | answer: true, 36 | }, 37 | }, 38 | insights: { 39 | select: { 40 | id: true, 41 | insight: true, 42 | reference: true, 43 | }, 44 | }, 45 | deadlines: { 46 | select: { 47 | id: true, 48 | description: true, 49 | dueDate: true, 50 | }, 51 | }, 52 | attendees: { 53 | select: { 54 | id: true, 55 | name: true, 56 | role: true, 57 | }, 58 | }, 59 | followUps: { 60 | select: { 61 | id: true, 62 | description: true, 63 | owner: true, 64 | }, 65 | }, 66 | risks: { 67 | select: { 68 | id: true, 69 | risk: true, 70 | impact: true, 71 | }, 72 | }, 73 | agenda: { 74 | select: { 75 | id: true, 76 | item: true, 77 | }, 78 | }, 79 | }, 80 | }) 81 | 82 | return NextResponse.json(meetings, { status: 200 }) 83 | } catch (error: any) { 84 | console.error(error) 85 | return NextResponse.json({ error: 'Failed to fetch meetings.' }, { status: 500 }) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /app/api/monitored-files/[fileName]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | 5 | const MONITORED_FOLDER = path.join(process.cwd(), 'public', 'monitored'); 6 | 7 | export async function GET(request: NextRequest, { params }: { params: { fileName: string } }) { 8 | const fileName = params.fileName; 9 | const filePath = path.join(MONITORED_FOLDER, fileName); 10 | 11 | try { 12 | if (!fs.existsSync(filePath)) { 13 | return NextResponse.json({ error: 'File not found' }, { status: 404 }); 14 | } 15 | 16 | const fileBuffer = fs.readFileSync(filePath); 17 | const fileType = path.extname(fileName).slice(1); 18 | 19 | return new NextResponse(fileBuffer, { 20 | headers: { 21 | 'Content-Type': `audio/${fileType}`, 22 | 'Content-Disposition': `attachment; filename=${fileName}`, 23 | }, 24 | }); 25 | } catch (error) { 26 | console.error('Error reading file:', error); 27 | return NextResponse.json({ error: 'Failed to read file' }, { status: 500 }); 28 | } 29 | } -------------------------------------------------------------------------------- /app/api/monitored-files/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | 5 | const MONITORED_FOLDER = path.join(process.cwd(), 'public', 'monitored'); 6 | 7 | export async function GET() { 8 | try { 9 | // Ensure the monitored folder exists 10 | if (!fs.existsSync(MONITORED_FOLDER)) { 11 | fs.mkdirSync(MONITORED_FOLDER, { recursive: true }); 12 | } 13 | 14 | // Read files from the monitored folder 15 | const files = fs.readdirSync(MONITORED_FOLDER); 16 | 17 | // Get file details 18 | const fileDetails = files.map(file => { 19 | const filePath = path.join(MONITORED_FOLDER, file); 20 | const stats = fs.statSync(filePath); 21 | return { 22 | name: file, 23 | size: stats.size, 24 | lastModified: stats.mtime.toISOString() 25 | }; 26 | }); 27 | 28 | return NextResponse.json(fileDetails); 29 | } catch (error) { 30 | console.error('Error reading monitored folder:', error); 31 | return NextResponse.json({ error: 'Failed to read monitored folder' }, { status: 500 }); 32 | } 33 | } -------------------------------------------------------------------------------- /app/api/transcribe/route.ts: -------------------------------------------------------------------------------- 1 | import type { NextRequest } from 'next/server' 2 | import { NextResponse } from 'next/server' 3 | import axios from 'axios' 4 | import fs from 'fs' 5 | import path from 'path' 6 | import { prisma } from '@/lib/prisma' 7 | 8 | export const POST = async (request: NextRequest) => { 9 | try { 10 | console.log('Received POST request to /api/transcribe') 11 | const formData = await request.formData() 12 | const file = formData.get('audio') as File 13 | const fullPath = formData.get('fullPath') as string 14 | 15 | console.log('Received file:', file?.name) 16 | console.log('Full path:', fullPath) 17 | 18 | if (!file) { 19 | console.error('No audio file provided') 20 | return NextResponse.json({ error: 'No audio file provided.' }, { status: 400 }) 21 | } 22 | 23 | // Convert File to Buffer 24 | const arrayBuffer = await file.arrayBuffer() 25 | const buffer = Buffer.from(arrayBuffer) 26 | 27 | // Define directory 28 | const uploadsDir = path.join(process.cwd(), 'public', 'uploads') 29 | 30 | // Ensure directory exists 31 | if (!fs.existsSync(uploadsDir)) { 32 | fs.mkdirSync(uploadsDir, { recursive: true }) 33 | } 34 | 35 | // Save the file 36 | const fileName = `${Date.now()}-${file.name}` 37 | const filePath = path.join(uploadsDir, fileName) 38 | fs.writeFileSync(filePath, buffer) 39 | 40 | console.log('File saved at:', filePath) 41 | 42 | // Get the API URL from the environment variable 43 | const apiUrl = process.env.LANGFLOW_FLOW_URL 44 | 45 | if (!apiUrl) { 46 | throw new Error('LANGFLOW_FLOW_URL is not defined in the environment variables') 47 | } 48 | 49 | // Prepare JSON payload 50 | const payload = { 51 | output_type: 'text', 52 | input_type: 'text', 53 | tweaks: { 54 | 'GroqWhisperComponent-Lep46': { 55 | audio_file: filePath // Use the full path of the saved file 56 | }, 57 | } 58 | } 59 | 60 | console.log('Sending request to API:', apiUrl) 61 | 62 | // Send POST request to the transcription API 63 | const apiResponse = await axios.post(apiUrl, payload, { 64 | headers: { 65 | 'Content-Type': 'application/json', 66 | }, 67 | }) 68 | 69 | console.log('Received response from API') 70 | 71 | // Ensure the response has the expected structure 72 | if (!apiResponse.data || !apiResponse.data.outputs) { 73 | throw new Error('Invalid API response structure.') 74 | } 75 | 76 | const { outputs } = apiResponse.data 77 | const analyzedTranscript = outputs[0]?.outputs[0]?.results?.breakdown?.text 78 | const rawTranscript = outputs[0]?.outputs[1]?.results?.transcription?.text 79 | 80 | if (!analyzedTranscript || !rawTranscript) { 81 | throw new Error('Invalid API response structure.') 82 | } 83 | console.log('Analyzed transcript:', analyzedTranscript.substring(0, 100) + '...') 84 | console.log('Raw transcript:', rawTranscript.substring(0, 100) + '...') 85 | 86 | // Parse JSON strings 87 | let analyzedData 88 | try { 89 | analyzedData = JSON.parse(analyzedTranscript) 90 | } catch (parseError) { 91 | throw new Error('Failed to parse analyzed transcript JSON.') 92 | } 93 | 94 | const rawData = rawTranscript 95 | console.log('Analyzed Data:', JSON.stringify(analyzedData, null, 2)) 96 | console.log('Saving to database...') 97 | 98 | // Helper function to format dates as ISO strings 99 | const formatDate = (date: string) => { 100 | const parsedDate = new Date(date) 101 | return !isNaN(parsedDate.getTime()) ? parsedDate.toISOString() : null 102 | } 103 | 104 | // Save to database with safe access 105 | const meeting = await prisma.meeting.create({ 106 | data: { 107 | name: analyzedData['Meeting Name'] || 'Untitled Meeting', 108 | description: analyzedData['Description'] || 'No description provided.', 109 | rawTranscript: rawData, 110 | summary: analyzedData['Summary'] || '', 111 | tasks: { 112 | create: (analyzedData['Tasks'] || []) 113 | .filter((task: any) => task && typeof task === 'object') 114 | .map((task: any) => ({ 115 | task: task.description || 'No task description', 116 | owner: task.owner || 'Unassigned', 117 | dueDate: task.due_date ? formatDate(task.due_date) : null, 118 | })), 119 | }, 120 | decisions: { 121 | create: (analyzedData['Decisions'] || []) 122 | .filter((decision: any) => decision && typeof decision === 'object') 123 | .map((decision: any) => ({ 124 | decision: decision.description || 'No decision description', 125 | date: decision.date ? formatDate(decision.date) : new Date().toISOString(), 126 | })), 127 | }, 128 | questions: { 129 | create: (analyzedData['Questions'] || []) 130 | .filter((question: any) => question && typeof question === 'object') 131 | .map((question: any) => ({ 132 | question: question.question || 'No question', 133 | status: question.status || 'Unanswered', 134 | answer: question.answer || '', 135 | })), 136 | }, 137 | insights: { 138 | create: (analyzedData['Insights'] || []) 139 | .filter((insight: any) => insight && typeof insight === 'object') 140 | .map((insight: any) => ({ 141 | insight: insight.insight || 'No insight', 142 | reference: insight.reference || '', 143 | })), 144 | }, 145 | deadlines: { 146 | create: (analyzedData['Deadlines'] || []) 147 | .filter((deadline: any) => deadline && typeof deadline === 'object') 148 | .map((deadline: any) => ({ 149 | description: deadline.description || 'No deadline description', 150 | dueDate: deadline.date ? formatDate(deadline.date) : null, 151 | })), 152 | }, 153 | attendees: { 154 | create: (analyzedData['Attendees'] || []) 155 | .filter((attendee: any) => attendee && typeof attendee === 'object') 156 | .map((attendee: any) => ({ 157 | name: attendee.name || 'Unnamed Attendee', 158 | role: attendee.role || 'No role specified', 159 | })), 160 | }, 161 | followUps: { 162 | create: (analyzedData['Follow-ups'] || []) 163 | .filter((followUp: any) => followUp && typeof followUp === 'object') 164 | .map((followUp: any) => ({ 165 | description: followUp.description || 'No follow-up description', 166 | owner: followUp.owner || 'Unassigned', 167 | })), 168 | }, 169 | risks: { 170 | create: (analyzedData['Risks'] || []) 171 | .filter((risk: any) => risk && typeof risk === 'object') 172 | .map((risk: any) => ({ 173 | risk: risk.risk || 'No risk description', 174 | impact: risk.impact || 'No impact specified', 175 | })), 176 | }, 177 | agenda: { 178 | create: (analyzedData['Agenda'] || []) 179 | .filter((item: any) => item && typeof item === 'string') 180 | .map((item: string) => ({ 181 | item: item, 182 | })), 183 | }, 184 | }, 185 | include: { 186 | tasks: true, 187 | decisions: true, 188 | questions: true, 189 | insights: true, 190 | deadlines: true, 191 | attendees: true, 192 | followUps: true, 193 | risks: true, 194 | agenda: true, 195 | }, 196 | }) 197 | 198 | console.log('Meeting saved successfully:', meeting.id) 199 | 200 | return NextResponse.json(meeting, { status: 200 }) 201 | } catch (error: any) { 202 | console.error('Error in /api/transcribe:', error) 203 | return NextResponse.json({ error: 'An error occurred during processing.' }, { status: 500 }) 204 | } 205 | } -------------------------------------------------------------------------------- /app/dashboard/meeting/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useEffect, useState } from "react"; 4 | import { useParams } from "next/navigation"; 5 | import axios from "axios"; 6 | import MeetingDetails from "@/components/MeetingDetails"; 7 | 8 | interface MeetingData { 9 | id: string; 10 | name: string; 11 | description: string; 12 | transcript: string; 13 | summary: string; 14 | breakdown: { 15 | Tasks: { task: string; owner: string; due_date: string }[]; 16 | Decisions: { decision: string; details: string }[]; 17 | Questions: { question: string; status: string; answer?: string }[]; 18 | Insights: { insight: string; reference: string }[]; 19 | Deadlines: { deadline: string; related_to: string }[]; 20 | Attendees: { name: string; role: string }[]; 21 | "Follow-ups": { follow_up: string; owner: string; due_date: string }[]; 22 | Risks: { risk: string; impact: string }[]; 23 | }; 24 | } 25 | import { useRouter } from 'next/navigation'; 26 | import { ArrowLeft } from 'lucide-react'; 27 | 28 | export default function MeetingPage() { 29 | const params = useParams(); 30 | const router = useRouter(); 31 | const meetingId = params.id as string; 32 | const [data, setData] = useState(null); 33 | const [error, setError] = useState(null); 34 | 35 | useEffect(() => { 36 | console.log("Fetching meeting details for ID:", meetingId); 37 | if (meetingId) { 38 | axios 39 | .get(`/api/meetings/${meetingId}`) 40 | .then((response) => { 41 | console.log("Received meeting data:", response.data); 42 | setData({ ...response.data, id: meetingId }); 43 | }) 44 | .catch((error) => { 45 | console.error("Error fetching meeting details:", error); 46 | if (error.response && error.response.status === 404) { 47 | setError("Meeting not found."); 48 | } else { 49 | setError("Failed to fetch meeting details."); 50 | } 51 | }); 52 | } 53 | }, [meetingId]); 54 | 55 | const handleGoBack = () => { 56 | router.push('/dashboard'); 57 | }; 58 | 59 | if (error) { 60 | return ( 61 |
62 | 66 |
{error}
67 |
68 | ); 69 | } 70 | 71 | if (!data) { 72 | return ( 73 |
74 | 78 | Loading... 79 |
80 | ); 81 | } 82 | 83 | console.log("Rendering MeetingDetails with data:", data); 84 | return ( 85 |
86 | 90 | 91 |
92 | ); 93 | } 94 | -------------------------------------------------------------------------------- /app/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useState, useEffect } from 'react'; 4 | import { Button } from "@/components/ui/button"; 5 | import { 6 | Table, 7 | TableBody, 8 | TableCell, 9 | TableHead, 10 | TableHeader, 11 | TableRow, 12 | } from "@/components/ui/table"; 13 | import { useRouter } from "next/navigation"; 14 | import axios from "axios"; 15 | import { Card, CardHeader, CardTitle, CardContent, CardFooter, CardDescription } from "@/components/ui/card"; 16 | import UploadAudio from '@/components/UploadAudio'; 17 | import { useToast } from "@/hooks/use-toast" 18 | 19 | 20 | interface Meeting { 21 | id: string; 22 | name: string; 23 | description: string; 24 | fileName: string; 25 | } 26 | 27 | const Dashboard: React.FC = () => { 28 | const router = useRouter(); 29 | const [meetings, setMeetings] = useState([]); 30 | const { toast } = useToast(); 31 | 32 | const fetchMeetings = async () => { 33 | try { 34 | const response = await axios.get("/api/meetings"); 35 | setMeetings(response.data); 36 | } catch (error) { 37 | console.error("Error fetching meetings:", error); 38 | setMeetings([]); 39 | toast({ 40 | title: 'Error', 41 | description: 'Failed to fetch meetings.', 42 | variant: 'destructive', 43 | }); 44 | } 45 | }; 46 | 47 | useEffect(() => { 48 | fetchMeetings(); 49 | }, []); 50 | 51 | const handleViewDetails = (meetingId: string) => { 52 | router.push(`dashboard/meeting/${meetingId}`); 53 | }; 54 | 55 | const handleDelete = async (meetingId: string) => { 56 | if (confirm("Are you sure you want to delete this meeting? This action cannot be undone.")) { 57 | try { 58 | await axios.delete(`/api/meetings/${meetingId}`); 59 | toast({ 60 | title: 'Success', 61 | description: 'Meeting deleted successfully.', 62 | }); 63 | fetchMeetings(); // Refresh the meetings list 64 | } catch (error) { 65 | console.error("Error deleting meeting:", error); 66 | toast({ 67 | title: 'Error', 68 | description: 'Failed to delete meeting.', 69 | variant: 'destructive', 70 | }); 71 | } 72 | } 73 | }; 74 | return ( 75 |
76 |
77 |

Meeting Mind

78 |
79 |
80 | 81 | 82 | 83 | Previous Meetings 84 | Recent transcribed meetings 85 | 86 | 87 | {meetings.length > 0 ? ( 88 | 89 | 90 | 91 | Name 92 | Description 93 | Actions 94 | 95 | 96 | 97 | {meetings.map((meeting) => ( 98 | 99 | {meeting.name} 100 | {meeting.description} 101 | 102 |
103 | 110 | 117 |
118 |
119 |
120 | ))} 121 |
122 |
123 | ) : ( 124 |

No meetings found.

125 | )} 126 |
127 |
128 |
129 |
130 | ); 131 | }; 132 | 133 | export default Dashboard; 134 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/misbahsy/meetingmind/6a3799316983086f61eb8f302264c4425aab8b50/app/favicon.ico -------------------------------------------------------------------------------- /app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/misbahsy/meetingmind/6a3799316983086f61eb8f302264c4425aab8b50/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/misbahsy/meetingmind/6a3799316983086f61eb8f302264c4425aab8b50/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | font-family: Arial, Helvetica, sans-serif; 7 | } 8 | 9 | @layer utilities { 10 | .text-balance { 11 | text-wrap: balance; 12 | } 13 | } 14 | 15 | @layer base { 16 | :root { 17 | --background: 0 0% 100%; 18 | --foreground: 0 0% 3.9%; 19 | --card: 0 0% 100%; 20 | --card-foreground: 0 0% 3.9%; 21 | --popover: 0 0% 100%; 22 | --popover-foreground: 0 0% 3.9%; 23 | --primary: 0 0% 9%; 24 | --primary-foreground: 0 0% 98%; 25 | --secondary: 0 0% 96.1%; 26 | --secondary-foreground: 0 0% 9%; 27 | --muted: 0 0% 96.1%; 28 | --muted-foreground: 0 0% 45.1%; 29 | --accent: 0 0% 96.1%; 30 | --accent-foreground: 0 0% 9%; 31 | --destructive: 0 84.2% 60.2%; 32 | --destructive-foreground: 0 0% 98%; 33 | --border: 0 0% 89.8%; 34 | --input: 0 0% 89.8%; 35 | --ring: 0 0% 3.9%; 36 | --chart-1: 12 76% 61%; 37 | --chart-2: 173 58% 39%; 38 | --chart-3: 197 37% 24%; 39 | --chart-4: 43 74% 66%; 40 | --chart-5: 27 87% 67%; 41 | --radius: 0.5rem; 42 | } 43 | .dark { 44 | --background: 0 0% 3.9%; 45 | --foreground: 0 0% 98%; 46 | --card: 0 0% 3.9%; 47 | --card-foreground: 0 0% 98%; 48 | --popover: 0 0% 3.9%; 49 | --popover-foreground: 0 0% 98%; 50 | --primary: 0 0% 98%; 51 | --primary-foreground: 0 0% 9%; 52 | --secondary: 0 0% 14.9%; 53 | --secondary-foreground: 0 0% 98%; 54 | --muted: 0 0% 14.9%; 55 | --muted-foreground: 0 0% 63.9%; 56 | --accent: 0 0% 14.9%; 57 | --accent-foreground: 0 0% 98%; 58 | --destructive: 0 62.8% 30.6%; 59 | --destructive-foreground: 0 0% 98%; 60 | --border: 0 0% 14.9%; 61 | --input: 0 0% 14.9%; 62 | --ring: 0 0% 83.1%; 63 | --chart-1: 220 70% 50%; 64 | --chart-2: 160 60% 45%; 65 | --chart-3: 30 80% 55%; 66 | --chart-4: 280 65% 60%; 67 | --chart-5: 340 75% 55%; 68 | } 69 | } 70 | 71 | @layer base { 72 | * { 73 | @apply border-border; 74 | } 75 | body { 76 | @apply bg-background text-foreground; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import localFont from "next/font/local"; 3 | import "./globals.css"; 4 | import { Toaster } from "@/components/ui/toaster"; 5 | 6 | const geistSans = localFont({ 7 | src: "./fonts/GeistVF.woff", 8 | variable: "--font-geist-sans", 9 | weight: "100 900", 10 | }); 11 | const geistMono = localFont({ 12 | src: "./fonts/GeistMonoVF.woff", 13 | variable: "--font-geist-mono", 14 | weight: "100 900", 15 | }); 16 | 17 | export const metadata: Metadata = { 18 | title: "Create Next App", 19 | description: "Generated by create next app", 20 | }; 21 | 22 | export default function RootLayout({ 23 | children, 24 | }: Readonly<{ 25 | children: React.ReactNode; 26 | }>) { 27 | return ( 28 | 29 | 32 | {children} 33 | 34 | 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useState, useEffect } from 'react' 4 | import Link from 'next/link' 5 | import { motion } from 'framer-motion' 6 | import { Mic, FileAudio, Brain, Clock, CheckCircle } from 'lucide-react' 7 | 8 | export default function Home() { 9 | const [isVisible, setIsVisible] = useState(false) 10 | 11 | useEffect(() => { 12 | setIsVisible(true) 13 | }, []) 14 | 15 | const fadeIn = { 16 | hidden: { opacity: 0, y: 20 }, 17 | visible: { opacity: 1, y: 0 } 18 | } 19 | 20 | return ( 21 |
22 |
23 |
24 | 25 | MeetingMind 26 |
27 | 32 |
33 | 34 |
35 |
36 | 43 | Transform Your Meetings with AI 44 | 45 | 52 | Capture, analyze, and act on your meeting insights effortlessly 53 | 54 | 60 | 61 | Get Started 62 | 63 | 64 |
65 | 66 |
67 |

How It Works

68 |
69 | {[ 70 | { icon: FileAudio, title: "Upload Audio", description: "Simply upload your meeting recording" }, 71 | { icon: Brain, title: "AI Analysis", description: "Our AI processes and extracts key information" }, 72 | { icon: CheckCircle, title: "Get Insights", description: "Review tasks, decisions, and action items" } 73 | ].map((step, index) => ( 74 | 82 | 83 |

{step.title}

84 |

{step.description}

85 |
86 | ))} 87 |
88 |
89 | 90 |
91 |

Benefits

92 |
93 | {[ 94 | { title: "Save Time", description: "Automatically extract key information from your meetings" }, 95 | { title: "Increase Productivity", description: "Focus on action items and decisions, not note-taking" }, 96 | { title: "Never Miss a Detail", description: "Capture every important point with AI-powered analysis" }, 97 | { title: "Easy Collaboration", description: "Share meeting insights with your team effortlessly" } 98 | ].map((benefit, index) => ( 99 | 107 |

{benefit.title}

108 |

{benefit.description}

109 |
110 | ))} 111 |
112 |
113 |
114 | 115 |
116 |
117 |

© 2024 MeetingMind. All rights reserved.

118 |
119 |
120 |
121 | ) 122 | } -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | } 20 | } -------------------------------------------------------------------------------- /components/CategoryCard.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import React from "react" 4 | import { ScrollArea } from "@/components/ui/scroll-area" 5 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" 6 | 7 | interface CategoryItem { 8 | [key: string]: string 9 | } 10 | 11 | interface CategoryProps { 12 | title: string 13 | items: CategoryItem[] 14 | gridSpan?: string 15 | } 16 | 17 | const CategoryCard: React.FC = ({ title, items, gridSpan }) => { 18 | return ( 19 | 20 | 21 | {title} 22 | 23 | 24 | 25 | {items.length === 0 ? ( 26 |

No items available.

27 | ) : ( 28 |
    29 | {items.map((item, index) => ( 30 |
  • 31 | {Object.entries(item).map(([key, value]) => { 32 | const formattedKey = key.split('_').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' '); 33 | let formattedValue = value; 34 | if (key === 'due_date' && value.includes('T')) { 35 | formattedValue = value.split('T')[0]; 36 | } 37 | return ( 38 |
    39 | {formattedKey}: {formattedValue} 40 |
    41 | ); 42 | })} 43 |
  • 44 | ))} 45 |
46 | )} 47 |
48 |
49 |
50 | ) 51 | } 52 | 53 | export default CategoryCard 54 | -------------------------------------------------------------------------------- /components/MeetingDetails.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import React from "react" 4 | import { motion } from "framer-motion" 5 | import { ScrollArea } from "@/components/ui/scroll-area" 6 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" 7 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" 8 | import { 9 | CheckCircle, 10 | Flag, 11 | AlertCircle, 12 | Lightbulb, 13 | Calendar, 14 | Users, 15 | List, 16 | AlertTriangle, 17 | FileText, 18 | Download, 19 | } from "lucide-react" 20 | import CategoryCard from "@/components/CategoryCard" 21 | import axios from "axios" 22 | import { useToast } from "@/hooks/use-toast" 23 | 24 | interface CategoryItem { 25 | [key: string]: string 26 | } 27 | 28 | interface MeetingDetailsProps { 29 | data: { 30 | id: string 31 | name: string 32 | description: string 33 | transcript: string 34 | summary: string 35 | breakdown: { 36 | Tasks: { task: string; owner: string; due_date: string }[] 37 | Decisions: { decision: string; details: string }[] 38 | Questions: { question: string; status: string; answer?: string }[] 39 | Insights: { insight: string; reference: string }[] 40 | Deadlines: { deadline: string; related_to: string }[] 41 | Attendees: { name: string; role: string }[] 42 | "Follow-ups": { follow_up: string; owner: string; due_date: string }[] 43 | Risks: { risk: string; impact: string }[] 44 | } 45 | } 46 | } 47 | 48 | export default function MeetingDetails({ data }: MeetingDetailsProps) { 49 | const { toast } = useToast() 50 | 51 | const categories = [ 52 | { title: "Tasks", icon: CheckCircle, items: data.breakdown.Tasks || [], gridSpan: "col-span-2" }, 53 | { title: "Decisions", icon: Flag, items: data.breakdown.Decisions || [], gridSpan: "col-span-2" }, 54 | { title: "Questions", icon: AlertCircle, items: data.breakdown.Questions || [], gridSpan: "col-span-2" }, 55 | { title: "Insights", icon: Lightbulb, items: data.breakdown.Insights || [], gridSpan: "col-span-2" }, 56 | { title: "Deadlines", icon: Calendar, items: data.breakdown.Deadlines || [], gridSpan: "col-span-1" }, 57 | { title: "Attendees", icon: Users, items: data.breakdown.Attendees || [], gridSpan: "col-span-1" }, 58 | { title: "Follow-ups", icon: List, items: data.breakdown["Follow-ups"] || [], gridSpan: "col-span-2" }, 59 | { title: "Risks", icon: AlertTriangle, items: data.breakdown.Risks || [], gridSpan: "col-span-2" }, 60 | ] 61 | 62 | const handleExport = async () => { 63 | try { 64 | const response = await axios.get(`/api/meetings/${data.id}/export`, { 65 | responseType: 'blob', 66 | }) 67 | 68 | if (response.status === 200) { 69 | const url = window.URL.createObjectURL(new Blob([response.data])) 70 | const link = document.createElement('a') 71 | link.href = url 72 | link.setAttribute('download', `${data.name.replace(/\s+/g, '_')}_Details.docx`) 73 | document.body.appendChild(link) 74 | link.click() 75 | link.parentNode?.removeChild(link) 76 | toast({ 77 | title: "Success", 78 | description: "Meeting details exported successfully!", 79 | }) 80 | } 81 | } catch (error: any) { 82 | console.error(error) 83 | toast({ 84 | title: "Error", 85 | description: "Failed to export meeting details.", 86 | variant: "destructive", 87 | }) 88 | } 89 | } 90 | 91 | return ( 92 |
93 |
94 |

{data.name}

95 | 102 |
103 |

{data.description}

104 | 105 | 106 | Summary 107 | Details 108 | 109 | 110 |
111 | 112 | 113 | 114 | 115 | Summary 116 | 117 | 118 | 119 |

{data.summary}

120 |
121 |
122 | 123 | 124 | 125 | 126 | Transcript 127 | 128 | 129 | 130 | 131 |

{data.transcript}

132 |
133 |
134 |
135 |
136 |
137 | 138 |
139 | {categories.map((category, index) => ( 140 | 147 | 152 | 153 | ))} 154 |
155 |
156 |
157 |
158 | ) 159 | } 160 | -------------------------------------------------------------------------------- /components/UploadAudio.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useState, useEffect } from 'react'; 4 | import { Button } from "@/components/ui/button"; 5 | import { Input } from "@/components/ui/input"; 6 | import { Label } from "@/components/ui/label"; 7 | import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "@/components/ui/card"; 8 | import { useToast } from "@/hooks/use-toast" 9 | import { FFmpeg } from '@ffmpeg/ffmpeg'; 10 | import { fetchFile } from '@ffmpeg/util'; 11 | 12 | const ffmpeg = new FFmpeg(); 13 | 14 | interface MonitoredFile { 15 | name: string; 16 | size: number; 17 | lastModified: string; 18 | } 19 | 20 | const UploadAudio: React.FC<{ onUploadSuccess: () => void }> = ({ onUploadSuccess }) => { 21 | const [selectedFile, setSelectedFile] = useState(null); 22 | const [isCompressing, setIsCompressing] = useState(false); 23 | const [isTranscribing, setIsTranscribing] = useState(false); 24 | const [compressedFile, setCompressedFile] = useState(null); 25 | const [monitoredFiles, setMonitoredFiles] = useState([]); 26 | const { toast } = useToast(); 27 | 28 | useEffect(() => { 29 | fetchMonitoredFiles(); 30 | const interval = setInterval(fetchMonitoredFiles, 10000); // Poll every 10 seconds 31 | return () => clearInterval(interval); 32 | }, []); 33 | 34 | const fetchMonitoredFiles = async () => { 35 | try { 36 | const response = await fetch('/api/monitored-files'); 37 | if (response.ok) { 38 | const files: MonitoredFile[] = await response.json(); 39 | setMonitoredFiles(files); 40 | } 41 | } catch (error) { 42 | console.error('Error fetching monitored files:', error); 43 | } 44 | }; 45 | 46 | const handleFileChange = (event: React.ChangeEvent) => { 47 | const file = event.target.files?.[0]; 48 | if (file) { 49 | setSelectedFile(file); 50 | setCompressedFile(null); 51 | } 52 | }; 53 | 54 | const handleMonitoredFileSelect = async (fileName: string) => { 55 | try { 56 | const response = await fetch(`/api/monitored-files/${encodeURIComponent(fileName)}`); 57 | if (response.ok) { 58 | const blob = await response.blob(); 59 | const file = new File([blob], fileName, { type: blob.type }); 60 | setSelectedFile(file); 61 | setCompressedFile(null); 62 | } 63 | } catch (error) { 64 | console.error('Error selecting monitored file:', error); 65 | toast({ 66 | title: 'File Selection Failed', 67 | description: 'An error occurred while selecting the file.', 68 | variant: 'destructive', 69 | }); 70 | } 71 | }; 72 | 73 | const loadFFmpeg = async () => { 74 | if (!ffmpeg.loaded) { 75 | await ffmpeg.load(); 76 | } 77 | }; 78 | 79 | const compressAudio = async () => { 80 | if (!selectedFile) return; 81 | setIsCompressing(true); 82 | try { 83 | await loadFFmpeg(); 84 | await ffmpeg.writeFile('input_audio', await fetchFile(selectedFile)); 85 | 86 | // Set target bitrate to reduce file size 87 | // Adjust parameters as needed 88 | await ffmpeg.exec([ 89 | '-i', 90 | 'input_audio', 91 | '-ar', 92 | '16000', 93 | '-ac', 94 | '1', 95 | '-b:a', 96 | '16k', 97 | 'output_audio.mp3' 98 | ]); 99 | 100 | const data = await ffmpeg.readFile('output_audio.mp3'); 101 | const compressedBlob = new Blob([data], { type: 'audio/mpeg' }); 102 | const compressed = new File([compressedBlob], `compressed_${selectedFile.name}`, { 103 | type: 'audio/mpeg', 104 | }); 105 | 106 | setCompressedFile(compressed); 107 | toast({ 108 | title: 'Compression Successful', 109 | description: `File size reduced to ${(compressed.size / (1024 * 1024)).toFixed(2)} MB`, 110 | }); 111 | } catch (error) { 112 | console.error('Compression error:', error); 113 | toast({ 114 | title: 'Compression Failed', 115 | description: 'An error occurred while compressing the audio.', 116 | variant: 'destructive', 117 | }); 118 | } finally { 119 | setIsCompressing(false); 120 | } 121 | }; 122 | 123 | const handleTranscribe = async () => { 124 | const fileToUpload = compressedFile || selectedFile; 125 | if (!fileToUpload) return; 126 | 127 | setIsTranscribing(true); 128 | const formData = new FormData(); 129 | formData.append('audio', fileToUpload); 130 | formData.append('fullPath', fileToUpload.name); 131 | 132 | try { 133 | await fetch('/api/transcribe', { 134 | method: 'POST', 135 | body: formData, 136 | }); 137 | toast({ 138 | title: 'Transcription Started', 139 | description: 'Your audio is being transcribed.', 140 | }); 141 | onUploadSuccess(); 142 | } catch (error) { 143 | console.error('Transcription error:', error); 144 | toast({ 145 | title: 'Transcription Failed', 146 | description: 'An error occurred while transcribing the audio.', 147 | variant: 'destructive', 148 | }); 149 | } finally { 150 | setIsTranscribing(false); 151 | setSelectedFile(null); 152 | setCompressedFile(null); 153 | } 154 | }; 155 | 156 | const getFileSizeMB = (file: File | null): number => { 157 | return file ? file.size / (1024 ** 2) : 0; 158 | }; 159 | 160 | const isCompressionNeeded = selectedFile && getFileSizeMB(selectedFile) > 24; 161 | const isTranscribeDisabled = 162 | (selectedFile && getFileSizeMB(selectedFile) > 24 && (!compressedFile || getFileSizeMB(compressedFile) > 24)) || 163 | isCompressing || 164 | isTranscribing; 165 | 166 | return ( 167 | 168 | 169 | Upload New Audio 170 | Select an audio file to transcribe 171 | 172 | 173 |
174 | 175 | 181 | {selectedFile && ( 182 |

Selected File: {selectedFile.name} ({getFileSizeMB(selectedFile).toFixed(2)} MB)

183 | )} 184 | {monitoredFiles.length > 0 && ( 185 |
186 |

Monitored Files:

187 |
    188 | {monitoredFiles.map((file) => ( 189 |
  • 190 | {file.name} ({(file.size / (1024 * 1024)).toFixed(2)} MB) 191 | 192 |
  • 193 | ))} 194 |
195 |
196 | )} 197 | {isCompressionNeeded && ( 198 | 201 | )} 202 | {compressedFile && ( 203 |

Compressed File: {compressedFile.name} ({getFileSizeMB(compressedFile).toFixed(2)} MB)

204 | )} 205 | 208 | {isTranscribeDisabled && ( 209 |

210 | {selectedFile && getFileSizeMB(compressedFile || selectedFile) > 24 211 | ? 'File size exceeds 24 MB, please compress the file before transcribing.' 212 | : ''} 213 |

214 | )} 215 |
216 |
217 |
218 | ); 219 | }; 220 | 221 | export default UploadAudio; 222 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ) 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button" 46 | return ( 47 | 52 | ) 53 | } 54 | ) 55 | Button.displayName = "Button" 56 | 57 | export { Button, buttonVariants } 58 | -------------------------------------------------------------------------------- /components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

41 | )) 42 | CardTitle.displayName = "CardTitle" 43 | 44 | const CardDescription = React.forwardRef< 45 | HTMLParagraphElement, 46 | React.HTMLAttributes 47 | >(({ className, ...props }, ref) => ( 48 |

53 | )) 54 | CardDescription.displayName = "CardDescription" 55 | 56 | const CardContent = React.forwardRef< 57 | HTMLDivElement, 58 | React.HTMLAttributes 59 | >(({ className, ...props }, ref) => ( 60 |

61 | )) 62 | CardContent.displayName = "CardContent" 63 | 64 | const CardFooter = React.forwardRef< 65 | HTMLDivElement, 66 | React.HTMLAttributes 67 | >(({ className, ...props }, ref) => ( 68 |
73 | )) 74 | CardFooter.displayName = "CardFooter" 75 | 76 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 77 | -------------------------------------------------------------------------------- /components/ui/dropdown-menu.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" 5 | import { 6 | CheckIcon, 7 | ChevronRightIcon, 8 | DotFilledIcon, 9 | } from "@radix-ui/react-icons" 10 | 11 | import { cn } from "@/lib/utils" 12 | 13 | const DropdownMenu = DropdownMenuPrimitive.Root 14 | 15 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger 16 | 17 | const DropdownMenuGroup = DropdownMenuPrimitive.Group 18 | 19 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal 20 | 21 | const DropdownMenuSub = DropdownMenuPrimitive.Sub 22 | 23 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup 24 | 25 | const DropdownMenuSubTrigger = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef & { 28 | inset?: boolean 29 | } 30 | >(({ className, inset, children, ...props }, ref) => ( 31 | 40 | {children} 41 | 42 | 43 | )) 44 | DropdownMenuSubTrigger.displayName = 45 | DropdownMenuPrimitive.SubTrigger.displayName 46 | 47 | const DropdownMenuSubContent = React.forwardRef< 48 | React.ElementRef, 49 | React.ComponentPropsWithoutRef 50 | >(({ className, ...props }, ref) => ( 51 | 59 | )) 60 | DropdownMenuSubContent.displayName = 61 | DropdownMenuPrimitive.SubContent.displayName 62 | 63 | const DropdownMenuContent = React.forwardRef< 64 | React.ElementRef, 65 | React.ComponentPropsWithoutRef 66 | >(({ className, sideOffset = 4, ...props }, ref) => ( 67 | 68 | 78 | 79 | )) 80 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName 81 | 82 | const DropdownMenuItem = React.forwardRef< 83 | React.ElementRef, 84 | React.ComponentPropsWithoutRef & { 85 | inset?: boolean 86 | } 87 | >(({ className, inset, ...props }, ref) => ( 88 | 97 | )) 98 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName 99 | 100 | const DropdownMenuCheckboxItem = React.forwardRef< 101 | React.ElementRef, 102 | React.ComponentPropsWithoutRef 103 | >(({ className, children, checked, ...props }, ref) => ( 104 | 113 | 114 | 115 | 116 | 117 | 118 | {children} 119 | 120 | )) 121 | DropdownMenuCheckboxItem.displayName = 122 | DropdownMenuPrimitive.CheckboxItem.displayName 123 | 124 | const DropdownMenuRadioItem = React.forwardRef< 125 | React.ElementRef, 126 | React.ComponentPropsWithoutRef 127 | >(({ className, children, ...props }, ref) => ( 128 | 136 | 137 | 138 | 139 | 140 | 141 | {children} 142 | 143 | )) 144 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName 145 | 146 | const DropdownMenuLabel = React.forwardRef< 147 | React.ElementRef, 148 | React.ComponentPropsWithoutRef & { 149 | inset?: boolean 150 | } 151 | >(({ className, inset, ...props }, ref) => ( 152 | 161 | )) 162 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName 163 | 164 | const DropdownMenuSeparator = React.forwardRef< 165 | React.ElementRef, 166 | React.ComponentPropsWithoutRef 167 | >(({ className, ...props }, ref) => ( 168 | 173 | )) 174 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName 175 | 176 | const DropdownMenuShortcut = ({ 177 | className, 178 | ...props 179 | }: React.HTMLAttributes) => { 180 | return ( 181 | 185 | ) 186 | } 187 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut" 188 | 189 | export { 190 | DropdownMenu, 191 | DropdownMenuTrigger, 192 | DropdownMenuContent, 193 | DropdownMenuItem, 194 | DropdownMenuCheckboxItem, 195 | DropdownMenuRadioItem, 196 | DropdownMenuLabel, 197 | DropdownMenuSeparator, 198 | DropdownMenuShortcut, 199 | DropdownMenuGroup, 200 | DropdownMenuPortal, 201 | DropdownMenuSub, 202 | DropdownMenuSubContent, 203 | DropdownMenuSubTrigger, 204 | DropdownMenuRadioGroup, 205 | } 206 | -------------------------------------------------------------------------------- /components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const ScrollArea = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, children, ...props }, ref) => ( 12 | 17 | 18 | {children} 19 | 20 | 21 | 22 | 23 | )) 24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName 25 | 26 | const ScrollBar = React.forwardRef< 27 | React.ElementRef, 28 | React.ComponentPropsWithoutRef 29 | >(({ className, orientation = "vertical", ...props }, ref) => ( 30 | 43 | 44 | 45 | )) 46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName 47 | 48 | export { ScrollArea, ScrollBar } 49 | -------------------------------------------------------------------------------- /components/ui/table.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Table = React.forwardRef< 6 | HTMLTableElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
10 | 15 | 16 | )) 17 | Table.displayName = "Table" 18 | 19 | const TableHeader = React.forwardRef< 20 | HTMLTableSectionElement, 21 | React.HTMLAttributes 22 | >(({ className, ...props }, ref) => ( 23 | 24 | )) 25 | TableHeader.displayName = "TableHeader" 26 | 27 | const TableBody = React.forwardRef< 28 | HTMLTableSectionElement, 29 | React.HTMLAttributes 30 | >(({ className, ...props }, ref) => ( 31 | 36 | )) 37 | TableBody.displayName = "TableBody" 38 | 39 | const TableFooter = React.forwardRef< 40 | HTMLTableSectionElement, 41 | React.HTMLAttributes 42 | >(({ className, ...props }, ref) => ( 43 | tr]:last:border-b-0", 47 | className 48 | )} 49 | {...props} 50 | /> 51 | )) 52 | TableFooter.displayName = "TableFooter" 53 | 54 | const TableRow = React.forwardRef< 55 | HTMLTableRowElement, 56 | React.HTMLAttributes 57 | >(({ className, ...props }, ref) => ( 58 | 66 | )) 67 | TableRow.displayName = "TableRow" 68 | 69 | const TableHead = React.forwardRef< 70 | HTMLTableCellElement, 71 | React.ThHTMLAttributes 72 | >(({ className, ...props }, ref) => ( 73 |
[role=checkbox]]:translate-y-[2px]", 77 | className 78 | )} 79 | {...props} 80 | /> 81 | )) 82 | TableHead.displayName = "TableHead" 83 | 84 | const TableCell = React.forwardRef< 85 | HTMLTableCellElement, 86 | React.TdHTMLAttributes 87 | >(({ className, ...props }, ref) => ( 88 | [role=checkbox]]:translate-y-[2px]", 92 | className 93 | )} 94 | {...props} 95 | /> 96 | )) 97 | TableCell.displayName = "TableCell" 98 | 99 | const TableCaption = React.forwardRef< 100 | HTMLTableCaptionElement, 101 | React.HTMLAttributes 102 | >(({ className, ...props }, ref) => ( 103 |
108 | )) 109 | TableCaption.displayName = "TableCaption" 110 | 111 | export { 112 | Table, 113 | TableHeader, 114 | TableBody, 115 | TableFooter, 116 | TableHead, 117 | TableRow, 118 | TableCell, 119 | TableCaption, 120 | } 121 | -------------------------------------------------------------------------------- /components/ui/tabs.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as TabsPrimitive from "@radix-ui/react-tabs" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Tabs = TabsPrimitive.Root 9 | 10 | const TabsList = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, ...props }, ref) => ( 14 | 22 | )) 23 | TabsList.displayName = TabsPrimitive.List.displayName 24 | 25 | const TabsTrigger = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef 28 | >(({ className, ...props }, ref) => ( 29 | 37 | )) 38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName 39 | 40 | const TabsContent = React.forwardRef< 41 | React.ElementRef, 42 | React.ComponentPropsWithoutRef 43 | >(({ className, ...props }, ref) => ( 44 | 52 | )) 53 | TabsContent.displayName = TabsPrimitive.Content.displayName 54 | 55 | export { Tabs, TabsList, TabsTrigger, TabsContent } 56 | -------------------------------------------------------------------------------- /components/ui/toast.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { Cross2Icon } from "@radix-ui/react-icons" 5 | import * as ToastPrimitives from "@radix-ui/react-toast" 6 | import { cva, type VariantProps } from "class-variance-authority" 7 | 8 | import { cn } from "@/lib/utils" 9 | 10 | const ToastProvider = ToastPrimitives.Provider 11 | 12 | const ToastViewport = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef 15 | >(({ className, ...props }, ref) => ( 16 | 24 | )) 25 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName 26 | 27 | const toastVariants = cva( 28 | "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", 29 | { 30 | variants: { 31 | variant: { 32 | default: "border bg-background text-foreground", 33 | destructive: 34 | "destructive group border-destructive bg-destructive text-destructive-foreground", 35 | }, 36 | }, 37 | defaultVariants: { 38 | variant: "default", 39 | }, 40 | } 41 | ) 42 | 43 | const Toast = React.forwardRef< 44 | React.ElementRef, 45 | React.ComponentPropsWithoutRef & 46 | VariantProps 47 | >(({ className, variant, ...props }, ref) => { 48 | return ( 49 | 54 | ) 55 | }) 56 | Toast.displayName = ToastPrimitives.Root.displayName 57 | 58 | const ToastAction = React.forwardRef< 59 | React.ElementRef, 60 | React.ComponentPropsWithoutRef 61 | >(({ className, ...props }, ref) => ( 62 | 70 | )) 71 | ToastAction.displayName = ToastPrimitives.Action.displayName 72 | 73 | const ToastClose = React.forwardRef< 74 | React.ElementRef, 75 | React.ComponentPropsWithoutRef 76 | >(({ className, ...props }, ref) => ( 77 | 86 | 87 | 88 | )) 89 | ToastClose.displayName = ToastPrimitives.Close.displayName 90 | 91 | const ToastTitle = React.forwardRef< 92 | React.ElementRef, 93 | React.ComponentPropsWithoutRef 94 | >(({ className, ...props }, ref) => ( 95 | 100 | )) 101 | ToastTitle.displayName = ToastPrimitives.Title.displayName 102 | 103 | const ToastDescription = React.forwardRef< 104 | React.ElementRef, 105 | React.ComponentPropsWithoutRef 106 | >(({ className, ...props }, ref) => ( 107 | 112 | )) 113 | ToastDescription.displayName = ToastPrimitives.Description.displayName 114 | 115 | type ToastProps = React.ComponentPropsWithoutRef 116 | 117 | type ToastActionElement = React.ReactElement 118 | 119 | export { 120 | type ToastProps, 121 | type ToastActionElement, 122 | ToastProvider, 123 | ToastViewport, 124 | Toast, 125 | ToastTitle, 126 | ToastDescription, 127 | ToastClose, 128 | ToastAction, 129 | } 130 | -------------------------------------------------------------------------------- /components/ui/toaster.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useToast } from "@/hooks/use-toast" 4 | import { 5 | Toast, 6 | ToastClose, 7 | ToastDescription, 8 | ToastProvider, 9 | ToastTitle, 10 | ToastViewport, 11 | } from "@/components/ui/toast" 12 | 13 | export function Toaster() { 14 | const { toasts } = useToast() 15 | 16 | return ( 17 | 18 | {toasts.map(function ({ id, title, description, action, ...props }) { 19 | return ( 20 | 21 |
22 | {title && {title}} 23 | {description && ( 24 | {description} 25 | )} 26 |
27 | {action} 28 | 29 |
30 | ) 31 | })} 32 | 33 |
34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | services: 3 | database: 4 | image: nouchka/sqlite3:latest 5 | stdin_open: true 6 | tty: true 7 | volumes: 8 | - sqlite-data:/root/db/ 9 | 10 | postgres: 11 | image: postgres:16 12 | environment: 13 | POSTGRES_USER: langflow 14 | POSTGRES_PASSWORD: langflow 15 | POSTGRES_DB: langflow 16 | ports: 17 | - 5432:5432 18 | volumes: 19 | - postgres-langflow:/var/lib/postgresql/data 20 | 21 | langflow: 22 | container_name: langflow 23 | image: langflowai/langflow:latest 24 | ports: 25 | - 7860:7860 26 | environment: 27 | - LANGFLOW_DATABASE_URL=postgresql://langflow:langflow@postgres:5432/langflow 28 | depends_on: 29 | - database 30 | volumes: 31 | - langflow-data:/app/langflow 32 | 33 | volumes: 34 | sqlite-data: 35 | langflow-data: 36 | postgres-langflow: 37 | -------------------------------------------------------------------------------- /hooks/use-toast.ts: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | // Inspired by react-hot-toast library 4 | import * as React from "react" 5 | 6 | import type { 7 | ToastActionElement, 8 | ToastProps, 9 | } from "@/components/ui/toast" 10 | 11 | const TOAST_LIMIT = 1 12 | const TOAST_REMOVE_DELAY = 1000000 13 | 14 | type ToasterToast = ToastProps & { 15 | id: string 16 | title?: React.ReactNode 17 | description?: React.ReactNode 18 | action?: ToastActionElement 19 | } 20 | 21 | const actionTypes = { 22 | ADD_TOAST: "ADD_TOAST", 23 | UPDATE_TOAST: "UPDATE_TOAST", 24 | DISMISS_TOAST: "DISMISS_TOAST", 25 | REMOVE_TOAST: "REMOVE_TOAST", 26 | } as const 27 | 28 | let count = 0 29 | 30 | function genId() { 31 | count = (count + 1) % Number.MAX_SAFE_INTEGER 32 | return count.toString() 33 | } 34 | 35 | type ActionType = typeof actionTypes 36 | 37 | type Action = 38 | | { 39 | type: ActionType["ADD_TOAST"] 40 | toast: ToasterToast 41 | } 42 | | { 43 | type: ActionType["UPDATE_TOAST"] 44 | toast: Partial 45 | } 46 | | { 47 | type: ActionType["DISMISS_TOAST"] 48 | toastId?: ToasterToast["id"] 49 | } 50 | | { 51 | type: ActionType["REMOVE_TOAST"] 52 | toastId?: ToasterToast["id"] 53 | } 54 | 55 | interface State { 56 | toasts: ToasterToast[] 57 | } 58 | 59 | const toastTimeouts = new Map>() 60 | 61 | const addToRemoveQueue = (toastId: string) => { 62 | if (toastTimeouts.has(toastId)) { 63 | return 64 | } 65 | 66 | const timeout = setTimeout(() => { 67 | toastTimeouts.delete(toastId) 68 | dispatch({ 69 | type: "REMOVE_TOAST", 70 | toastId: toastId, 71 | }) 72 | }, TOAST_REMOVE_DELAY) 73 | 74 | toastTimeouts.set(toastId, timeout) 75 | } 76 | 77 | export const reducer = (state: State, action: Action): State => { 78 | switch (action.type) { 79 | case "ADD_TOAST": 80 | return { 81 | ...state, 82 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), 83 | } 84 | 85 | case "UPDATE_TOAST": 86 | return { 87 | ...state, 88 | toasts: state.toasts.map((t) => 89 | t.id === action.toast.id ? { ...t, ...action.toast } : t 90 | ), 91 | } 92 | 93 | case "DISMISS_TOAST": { 94 | const { toastId } = action 95 | 96 | // ! Side effects ! - This could be extracted into a dismissToast() action, 97 | // but I'll keep it here for simplicity 98 | if (toastId) { 99 | addToRemoveQueue(toastId) 100 | } else { 101 | state.toasts.forEach((toast) => { 102 | addToRemoveQueue(toast.id) 103 | }) 104 | } 105 | 106 | return { 107 | ...state, 108 | toasts: state.toasts.map((t) => 109 | t.id === toastId || toastId === undefined 110 | ? { 111 | ...t, 112 | open: false, 113 | } 114 | : t 115 | ), 116 | } 117 | } 118 | case "REMOVE_TOAST": 119 | if (action.toastId === undefined) { 120 | return { 121 | ...state, 122 | toasts: [], 123 | } 124 | } 125 | return { 126 | ...state, 127 | toasts: state.toasts.filter((t) => t.id !== action.toastId), 128 | } 129 | } 130 | } 131 | 132 | const listeners: Array<(state: State) => void> = [] 133 | 134 | let memoryState: State = { toasts: [] } 135 | 136 | function dispatch(action: Action) { 137 | memoryState = reducer(memoryState, action) 138 | listeners.forEach((listener) => { 139 | listener(memoryState) 140 | }) 141 | } 142 | 143 | type Toast = Omit 144 | 145 | function toast({ ...props }: Toast) { 146 | const id = genId() 147 | 148 | const update = (props: ToasterToast) => 149 | dispatch({ 150 | type: "UPDATE_TOAST", 151 | toast: { ...props, id }, 152 | }) 153 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) 154 | 155 | dispatch({ 156 | type: "ADD_TOAST", 157 | toast: { 158 | ...props, 159 | id, 160 | open: true, 161 | onOpenChange: (open) => { 162 | if (!open) dismiss() 163 | }, 164 | }, 165 | }) 166 | 167 | return { 168 | id: id, 169 | dismiss, 170 | update, 171 | } 172 | } 173 | 174 | function useToast() { 175 | const [state, setState] = React.useState(memoryState) 176 | 177 | React.useEffect(() => { 178 | listeners.push(setState) 179 | return () => { 180 | const index = listeners.indexOf(setState) 181 | if (index > -1) { 182 | listeners.splice(index, 1) 183 | } 184 | } 185 | }, [state]) 186 | 187 | return { 188 | ...state, 189 | toast, 190 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), 191 | } 192 | } 193 | 194 | export { useToast, toast } 195 | -------------------------------------------------------------------------------- /lib/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client' 2 | const globalForPrisma = global as unknown as { prisma: PrismaClient } 3 | export const prisma = 4 | globalForPrisma.prisma || 5 | new PrismaClient({ 6 | log: ['query'], 7 | }) 8 | if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "meeting-notes", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "prisma generate && next build", 7 | "dev": "next dev", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "prisma:studio": "prisma studio" 11 | }, 12 | "dependencies": { 13 | "@ffmpeg-installer/ffmpeg": "^1.1.0", 14 | "@ffmpeg/ffmpeg": "^0.12.10", 15 | "@ffmpeg/util": "^0.12.1", 16 | "@prisma/client": "^5.20.0", 17 | "@radix-ui/react-dropdown-menu": "^2.1.2", 18 | "@radix-ui/react-icons": "^1.3.0", 19 | "@radix-ui/react-label": "^2.1.0", 20 | "@radix-ui/react-scroll-area": "^1.2.0", 21 | "@radix-ui/react-slot": "^1.1.0", 22 | "@radix-ui/react-tabs": "^1.1.1", 23 | "@radix-ui/react-toast": "^1.2.2", 24 | "axios": "^1.4.0", 25 | "class-variance-authority": "^0.7.0", 26 | "clsx": "^2.1.1", 27 | "docx": "^9.0.2", 28 | "fluent-ffmpeg": "^2.1.3", 29 | "framer-motion": "^11.11.1", 30 | "lucide-react": "^0.447.0", 31 | "next": "14.2.14", 32 | "react": "^18", 33 | "react-dom": "^18", 34 | "react-mic": "^12.4.6", 35 | "sqlite3": "^5.1.7", 36 | "tailwind-merge": "^2.5.3", 37 | "tailwindcss-animate": "^1.0.7" 38 | }, 39 | "devDependencies": { 40 | "@types/node": "^20", 41 | "@types/react": "^18", 42 | "@types/react-dom": "^18", 43 | "eslint": "^8", 44 | "eslint-config-next": "14.2.14", 45 | "postcss": "^8", 46 | "prisma": "^5.20.0", 47 | "tailwindcss": "^3.4.1", 48 | "typescript": "^5" 49 | } 50 | } -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /prisma/dev.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/misbahsy/meetingmind/6a3799316983086f61eb8f302264c4425aab8b50/prisma/dev.db -------------------------------------------------------------------------------- /prisma/migrations/20241009233612_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Meeting" ( 3 | "id" TEXT NOT NULL PRIMARY KEY, 4 | "name" TEXT NOT NULL, 5 | "description" TEXT NOT NULL, 6 | "rawTranscript" TEXT NOT NULL, 7 | "summary" TEXT NOT NULL, 8 | "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 9 | "updatedAt" DATETIME NOT NULL 10 | ); 11 | 12 | -- CreateTable 13 | CREATE TABLE "Task" ( 14 | "id" TEXT NOT NULL PRIMARY KEY, 15 | "task" TEXT NOT NULL, 16 | "owner" TEXT NOT NULL, 17 | "dueDate" TEXT NOT NULL, 18 | "meetingId" TEXT NOT NULL, 19 | "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 20 | "updatedAt" DATETIME NOT NULL, 21 | CONSTRAINT "Task_meetingId_fkey" FOREIGN KEY ("meetingId") REFERENCES "Meeting" ("id") ON DELETE RESTRICT ON UPDATE CASCADE 22 | ); 23 | 24 | -- CreateTable 25 | CREATE TABLE "Decision" ( 26 | "id" TEXT NOT NULL PRIMARY KEY, 27 | "decision" TEXT NOT NULL, 28 | "details" TEXT NOT NULL, 29 | "meetingId" TEXT NOT NULL, 30 | "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 31 | "updatedAt" DATETIME NOT NULL, 32 | CONSTRAINT "Decision_meetingId_fkey" FOREIGN KEY ("meetingId") REFERENCES "Meeting" ("id") ON DELETE RESTRICT ON UPDATE CASCADE 33 | ); 34 | 35 | -- CreateTable 36 | CREATE TABLE "Question" ( 37 | "id" TEXT NOT NULL PRIMARY KEY, 38 | "question" TEXT NOT NULL, 39 | "status" TEXT NOT NULL, 40 | "answer" TEXT, 41 | "meetingId" TEXT NOT NULL, 42 | "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 43 | "updatedAt" DATETIME NOT NULL, 44 | CONSTRAINT "Question_meetingId_fkey" FOREIGN KEY ("meetingId") REFERENCES "Meeting" ("id") ON DELETE RESTRICT ON UPDATE CASCADE 45 | ); 46 | 47 | -- CreateTable 48 | CREATE TABLE "Insight" ( 49 | "id" TEXT NOT NULL PRIMARY KEY, 50 | "insight" TEXT NOT NULL, 51 | "reference" TEXT NOT NULL, 52 | "meetingId" TEXT NOT NULL, 53 | "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 54 | "updatedAt" DATETIME NOT NULL, 55 | CONSTRAINT "Insight_meetingId_fkey" FOREIGN KEY ("meetingId") REFERENCES "Meeting" ("id") ON DELETE RESTRICT ON UPDATE CASCADE 56 | ); 57 | 58 | -- CreateTable 59 | CREATE TABLE "Deadline" ( 60 | "id" TEXT NOT NULL PRIMARY KEY, 61 | "deadline" TEXT NOT NULL, 62 | "relatedTo" TEXT NOT NULL, 63 | "meetingId" TEXT NOT NULL, 64 | "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 65 | "updatedAt" DATETIME NOT NULL, 66 | CONSTRAINT "Deadline_meetingId_fkey" FOREIGN KEY ("meetingId") REFERENCES "Meeting" ("id") ON DELETE RESTRICT ON UPDATE CASCADE 67 | ); 68 | 69 | -- CreateTable 70 | CREATE TABLE "Attendee" ( 71 | "id" TEXT NOT NULL PRIMARY KEY, 72 | "name" TEXT NOT NULL, 73 | "role" TEXT NOT NULL, 74 | "meetingId" TEXT NOT NULL, 75 | "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 76 | "updatedAt" DATETIME NOT NULL, 77 | CONSTRAINT "Attendee_meetingId_fkey" FOREIGN KEY ("meetingId") REFERENCES "Meeting" ("id") ON DELETE RESTRICT ON UPDATE CASCADE 78 | ); 79 | 80 | -- CreateTable 81 | CREATE TABLE "FollowUp" ( 82 | "id" TEXT NOT NULL PRIMARY KEY, 83 | "followUp" TEXT NOT NULL, 84 | "owner" TEXT NOT NULL, 85 | "dueDate" TEXT NOT NULL, 86 | "meetingId" TEXT NOT NULL, 87 | "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 88 | "updatedAt" DATETIME NOT NULL, 89 | CONSTRAINT "FollowUp_meetingId_fkey" FOREIGN KEY ("meetingId") REFERENCES "Meeting" ("id") ON DELETE RESTRICT ON UPDATE CASCADE 90 | ); 91 | 92 | -- CreateTable 93 | CREATE TABLE "Risk" ( 94 | "id" TEXT NOT NULL PRIMARY KEY, 95 | "risk" TEXT NOT NULL, 96 | "impact" TEXT NOT NULL, 97 | "meetingId" TEXT NOT NULL, 98 | "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 99 | "updatedAt" DATETIME NOT NULL, 100 | CONSTRAINT "Risk_meetingId_fkey" FOREIGN KEY ("meetingId") REFERENCES "Meeting" ("id") ON DELETE RESTRICT ON UPDATE CASCADE 101 | ); 102 | -------------------------------------------------------------------------------- /prisma/migrations/20241010010041_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- RedefineTables 2 | PRAGMA defer_foreign_keys=ON; 3 | PRAGMA foreign_keys=OFF; 4 | CREATE TABLE "new_Deadline" ( 5 | "id" TEXT NOT NULL PRIMARY KEY, 6 | "deadline" TEXT, 7 | "relatedTo" TEXT NOT NULL, 8 | "meetingId" TEXT NOT NULL, 9 | "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 10 | "updatedAt" DATETIME NOT NULL, 11 | CONSTRAINT "Deadline_meetingId_fkey" FOREIGN KEY ("meetingId") REFERENCES "Meeting" ("id") ON DELETE RESTRICT ON UPDATE CASCADE 12 | ); 13 | INSERT INTO "new_Deadline" ("createdAt", "deadline", "id", "meetingId", "relatedTo", "updatedAt") SELECT "createdAt", "deadline", "id", "meetingId", "relatedTo", "updatedAt" FROM "Deadline"; 14 | DROP TABLE "Deadline"; 15 | ALTER TABLE "new_Deadline" RENAME TO "Deadline"; 16 | CREATE TABLE "new_FollowUp" ( 17 | "id" TEXT NOT NULL PRIMARY KEY, 18 | "followUp" TEXT NOT NULL, 19 | "owner" TEXT NOT NULL, 20 | "dueDate" TEXT, 21 | "meetingId" TEXT NOT NULL, 22 | "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 23 | "updatedAt" DATETIME NOT NULL, 24 | CONSTRAINT "FollowUp_meetingId_fkey" FOREIGN KEY ("meetingId") REFERENCES "Meeting" ("id") ON DELETE RESTRICT ON UPDATE CASCADE 25 | ); 26 | INSERT INTO "new_FollowUp" ("createdAt", "dueDate", "followUp", "id", "meetingId", "owner", "updatedAt") SELECT "createdAt", "dueDate", "followUp", "id", "meetingId", "owner", "updatedAt" FROM "FollowUp"; 27 | DROP TABLE "FollowUp"; 28 | ALTER TABLE "new_FollowUp" RENAME TO "FollowUp"; 29 | PRAGMA foreign_keys=ON; 30 | PRAGMA defer_foreign_keys=OFF; 31 | -------------------------------------------------------------------------------- /prisma/migrations/20241010054943_init/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `deadline` on the `Deadline` table. All the data in the column will be lost. 5 | - You are about to drop the column `relatedTo` on the `Deadline` table. All the data in the column will be lost. 6 | - You are about to drop the column `details` on the `Decision` table. All the data in the column will be lost. 7 | - You are about to drop the column `dueDate` on the `FollowUp` table. All the data in the column will be lost. 8 | - You are about to drop the column `followUp` on the `FollowUp` table. All the data in the column will be lost. 9 | - You are about to alter the column `dueDate` on the `Task` table. The data in that column could be lost. The data in that column will be cast from `String` to `DateTime`. 10 | - Added the required column `description` to the `Deadline` table without a default value. This is not possible if the table is not empty. 11 | - Added the required column `date` to the `Decision` table without a default value. This is not possible if the table is not empty. 12 | - Added the required column `description` to the `FollowUp` table without a default value. This is not possible if the table is not empty. 13 | 14 | */ 15 | -- CreateTable 16 | CREATE TABLE "AgendaItem" ( 17 | "id" TEXT NOT NULL PRIMARY KEY, 18 | "item" TEXT NOT NULL, 19 | "meetingId" TEXT NOT NULL, 20 | "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 21 | "updatedAt" DATETIME NOT NULL, 22 | CONSTRAINT "AgendaItem_meetingId_fkey" FOREIGN KEY ("meetingId") REFERENCES "Meeting" ("id") ON DELETE RESTRICT ON UPDATE CASCADE 23 | ); 24 | 25 | -- RedefineTables 26 | PRAGMA defer_foreign_keys=ON; 27 | PRAGMA foreign_keys=OFF; 28 | CREATE TABLE "new_Deadline" ( 29 | "id" TEXT NOT NULL PRIMARY KEY, 30 | "description" TEXT NOT NULL, 31 | "dueDate" DATETIME, 32 | "meetingId" TEXT NOT NULL, 33 | "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 34 | "updatedAt" DATETIME NOT NULL, 35 | CONSTRAINT "Deadline_meetingId_fkey" FOREIGN KEY ("meetingId") REFERENCES "Meeting" ("id") ON DELETE RESTRICT ON UPDATE CASCADE 36 | ); 37 | INSERT INTO "new_Deadline" ("createdAt", "id", "meetingId", "updatedAt") SELECT "createdAt", "id", "meetingId", "updatedAt" FROM "Deadline"; 38 | DROP TABLE "Deadline"; 39 | ALTER TABLE "new_Deadline" RENAME TO "Deadline"; 40 | CREATE TABLE "new_Decision" ( 41 | "id" TEXT NOT NULL PRIMARY KEY, 42 | "decision" TEXT NOT NULL, 43 | "date" DATETIME NOT NULL, 44 | "meetingId" TEXT NOT NULL, 45 | "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 46 | "updatedAt" DATETIME NOT NULL, 47 | CONSTRAINT "Decision_meetingId_fkey" FOREIGN KEY ("meetingId") REFERENCES "Meeting" ("id") ON DELETE RESTRICT ON UPDATE CASCADE 48 | ); 49 | INSERT INTO "new_Decision" ("createdAt", "decision", "id", "meetingId", "updatedAt") SELECT "createdAt", "decision", "id", "meetingId", "updatedAt" FROM "Decision"; 50 | DROP TABLE "Decision"; 51 | ALTER TABLE "new_Decision" RENAME TO "Decision"; 52 | CREATE TABLE "new_FollowUp" ( 53 | "id" TEXT NOT NULL PRIMARY KEY, 54 | "description" TEXT NOT NULL, 55 | "owner" TEXT NOT NULL, 56 | "meetingId" TEXT NOT NULL, 57 | "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 58 | "updatedAt" DATETIME NOT NULL, 59 | CONSTRAINT "FollowUp_meetingId_fkey" FOREIGN KEY ("meetingId") REFERENCES "Meeting" ("id") ON DELETE RESTRICT ON UPDATE CASCADE 60 | ); 61 | INSERT INTO "new_FollowUp" ("createdAt", "id", "meetingId", "owner", "updatedAt") SELECT "createdAt", "id", "meetingId", "owner", "updatedAt" FROM "FollowUp"; 62 | DROP TABLE "FollowUp"; 63 | ALTER TABLE "new_FollowUp" RENAME TO "FollowUp"; 64 | CREATE TABLE "new_Task" ( 65 | "id" TEXT NOT NULL PRIMARY KEY, 66 | "task" TEXT NOT NULL, 67 | "owner" TEXT NOT NULL, 68 | "dueDate" DATETIME, 69 | "meetingId" TEXT NOT NULL, 70 | "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 71 | "updatedAt" DATETIME NOT NULL, 72 | CONSTRAINT "Task_meetingId_fkey" FOREIGN KEY ("meetingId") REFERENCES "Meeting" ("id") ON DELETE RESTRICT ON UPDATE CASCADE 73 | ); 74 | INSERT INTO "new_Task" ("createdAt", "dueDate", "id", "meetingId", "owner", "task", "updatedAt") SELECT "createdAt", "dueDate", "id", "meetingId", "owner", "task", "updatedAt" FROM "Task"; 75 | DROP TABLE "Task"; 76 | ALTER TABLE "new_Task" RENAME TO "Task"; 77 | PRAGMA foreign_keys=ON; 78 | PRAGMA defer_foreign_keys=OFF; 79 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "sqlite" -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "sqlite" 10 | url = "file:./dev.db" 11 | } 12 | 13 | model Meeting { 14 | id String @id @default(uuid()) 15 | name String 16 | description String 17 | rawTranscript String 18 | summary String 19 | createdAt DateTime @default(now()) 20 | updatedAt DateTime @updatedAt 21 | 22 | tasks Task[] 23 | decisions Decision[] 24 | questions Question[] 25 | insights Insight[] 26 | deadlines Deadline[] 27 | attendees Attendee[] 28 | followUps FollowUp[] 29 | risks Risk[] 30 | agenda AgendaItem[] 31 | } 32 | 33 | model Task { 34 | id String @id @default(uuid()) 35 | task String 36 | owner String 37 | dueDate DateTime? 38 | meetingId String 39 | createdAt DateTime @default(now()) 40 | updatedAt DateTime @updatedAt 41 | 42 | meeting Meeting @relation(fields: [meetingId], references: [id]) 43 | } 44 | 45 | model Decision { 46 | id String @id @default(uuid()) 47 | decision String 48 | date DateTime 49 | meetingId String 50 | createdAt DateTime @default(now()) 51 | updatedAt DateTime @updatedAt 52 | 53 | meeting Meeting @relation(fields: [meetingId], references: [id]) 54 | } 55 | 56 | model Question { 57 | id String @id @default(uuid()) 58 | question String 59 | status String 60 | answer String? 61 | meetingId String 62 | createdAt DateTime @default(now()) 63 | updatedAt DateTime @updatedAt 64 | 65 | meeting Meeting @relation(fields: [meetingId], references: [id]) 66 | } 67 | 68 | model Insight { 69 | id String @id @default(uuid()) 70 | insight String 71 | reference String 72 | meetingId String 73 | createdAt DateTime @default(now()) 74 | updatedAt DateTime @updatedAt 75 | 76 | meeting Meeting @relation(fields: [meetingId], references: [id]) 77 | } 78 | 79 | model Deadline { 80 | id String @id @default(uuid()) 81 | description String 82 | dueDate DateTime? 83 | meetingId String 84 | createdAt DateTime @default(now()) 85 | updatedAt DateTime @updatedAt 86 | 87 | meeting Meeting @relation(fields: [meetingId], references: [id]) 88 | } 89 | 90 | model Attendee { 91 | id String @id @default(uuid()) 92 | name String 93 | role String 94 | meetingId String 95 | createdAt DateTime @default(now()) 96 | updatedAt DateTime @updatedAt 97 | 98 | meeting Meeting @relation(fields: [meetingId], references: [id]) 99 | } 100 | 101 | model FollowUp { 102 | id String @id @default(uuid()) 103 | description String 104 | owner String 105 | meetingId String 106 | createdAt DateTime @default(now()) 107 | updatedAt DateTime @updatedAt 108 | 109 | meeting Meeting @relation(fields: [meetingId], references: [id]) 110 | } 111 | 112 | model Risk { 113 | id String @id @default(uuid()) 114 | risk String 115 | impact String 116 | meetingId String 117 | createdAt DateTime @default(now()) 118 | updatedAt DateTime @updatedAt 119 | 120 | meeting Meeting @relation(fields: [meetingId], references: [id]) 121 | } 122 | 123 | model AgendaItem { 124 | id String @id @default(uuid()) 125 | item String 126 | meetingId String 127 | createdAt DateTime @default(now()) 128 | updatedAt DateTime @updatedAt 129 | 130 | meeting Meeting @relation(fields: [meetingId], references: [id]) 131 | } -------------------------------------------------------------------------------- /public/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/misbahsy/meetingmind/6a3799316983086f61eb8f302264c4425aab8b50/public/dashboard.png -------------------------------------------------------------------------------- /public/landing.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/misbahsy/meetingmind/6a3799316983086f61eb8f302264c4425aab8b50/public/landing.webp -------------------------------------------------------------------------------- /public/meeting_details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/misbahsy/meetingmind/6a3799316983086f61eb8f302264c4425aab8b50/public/meeting_details.png -------------------------------------------------------------------------------- /public/meeting_summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/misbahsy/meetingmind/6a3799316983086f61eb8f302264c4425aab8b50/public/meeting_summary.png -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | darkMode: ["class"], 5 | content: [ 6 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 8 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 9 | ], 10 | theme: { 11 | extend: { 12 | colors: { 13 | background: 'hsl(var(--background))', 14 | foreground: 'hsl(var(--foreground))', 15 | card: { 16 | DEFAULT: 'hsl(var(--card))', 17 | foreground: 'hsl(var(--card-foreground))' 18 | }, 19 | popover: { 20 | DEFAULT: 'hsl(var(--popover))', 21 | foreground: 'hsl(var(--popover-foreground))' 22 | }, 23 | primary: { 24 | DEFAULT: 'hsl(var(--primary))', 25 | foreground: 'hsl(var(--primary-foreground))' 26 | }, 27 | secondary: { 28 | DEFAULT: 'hsl(var(--secondary))', 29 | foreground: 'hsl(var(--secondary-foreground))' 30 | }, 31 | muted: { 32 | DEFAULT: 'hsl(var(--muted))', 33 | foreground: 'hsl(var(--muted-foreground))' 34 | }, 35 | accent: { 36 | DEFAULT: 'hsl(var(--accent))', 37 | foreground: 'hsl(var(--accent-foreground))' 38 | }, 39 | destructive: { 40 | DEFAULT: 'hsl(var(--destructive))', 41 | foreground: 'hsl(var(--destructive-foreground))' 42 | }, 43 | border: 'hsl(var(--border))', 44 | input: 'hsl(var(--input))', 45 | ring: 'hsl(var(--ring))', 46 | chart: { 47 | '1': 'hsl(var(--chart-1))', 48 | '2': 'hsl(var(--chart-2))', 49 | '3': 'hsl(var(--chart-3))', 50 | '4': 'hsl(var(--chart-4))', 51 | '5': 'hsl(var(--chart-5))' 52 | } 53 | }, 54 | borderRadius: { 55 | lg: 'var(--radius)', 56 | md: 'calc(var(--radius) - 2px)', 57 | sm: 'calc(var(--radius) - 4px)' 58 | } 59 | } 60 | }, 61 | plugins: [require("tailwindcss-animate")], 62 | }; 63 | export default config; 64 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /utils/demo/MeetingMind.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/misbahsy/meetingmind/6a3799316983086f61eb8f302264c4425aab8b50/utils/demo/MeetingMind.mp4 -------------------------------------------------------------------------------- /utils/langflow_flow/Meeting Mind.json: -------------------------------------------------------------------------------- 1 | {"id":"5781a690-e689-4b26-b636-45da76a91915","data":{"nodes":[{"id":"GroqWhisperComponent-Lep46","type":"genericNode","position":{"x":-1483.4857952490108,"y":37.275916175740406},"data":{"type":"GroqWhisperComponent","node":{"template":{"_type":"Component","audio_file":{"trace_as_metadata":true,"file_path":"5781a690-e689-4b26-b636-45da76a91915/2024-10-09_11-04-49_test2.m4a","fileTypes":["mp3","mp4","m4a","wav","webm","m4b","mpga","mp2","flac"],"list":false,"required":false,"placeholder":"","show":true,"name":"audio_file","value":"","display_name":"Audio File","advanced":false,"dynamic":false,"info":"Supported file types: mp3, mp4, m4a, wav, webm, m4b, mpga, mp2, flac","title_case":false,"type":"file","_input_type":"FileInput"},"code":{"type":"code","required":true,"placeholder":"","list":false,"show":true,"multiline":true,"value":"from langflow.custom import Component\nfrom langflow.inputs import (\n FileInput,\n SecretStrInput,\n DropdownInput,\n StrInput,\n FloatInput,\n)\nfrom langflow.io import BoolInput, IntInput, MessageTextInput\nfrom langflow.template import Output\nfrom langflow.schema.message import Message\nfrom groq import Groq\n\nclass GroqWhisperComponent(Component):\n display_name = \"Groq Whisper\"\n description = \"Audio to text with Whisper model using Groq API\"\n icon = \"file-audio\"\n inputs = [\n SecretStrInput(\n name=\"groq_api_key\",\n display_name=\"Groq API Key\",\n info=\"The Groq API Key to use for the Whisper model.\",\n advanced=False,\n ),\n FileInput(\n name=\"audio_file\",\n display_name=\"Audio File\",\n file_types=[\"mp3\", \"mp4\", \"m4a\", \"wav\", \"webm\", \"m4b\", \"mpga\", \"mp2\", \"flac\"],\n info=\"Supported file types: mp3, mp4, m4a, wav, webm, m4b, mpga, mp2, flac\",\n ),\n DropdownInput(\n name=\"model_name\",\n display_name=\"Model\",\n info=\"The name of the model to use.\",\n options=[\"distil-whisper-large-v3-en\"],\n value=\"distil-whisper-large-v3-en\",\n ),\n MessageTextInput(\n name=\"prompt\",\n display_name=\"Prompt\",\n info=\"An optional text to guide the model's style or continue a previous audio segment.\",\n advanced=True,\n value=\"\",\n ),\n StrInput(\n name=\"language\",\n display_name=\"Language\",\n info=\"The language of the input audio. Supplying the input language in ISO-639-1 format will improve accuracy and latency.\",\n advanced=True,\n value=\"\",\n ),\n FloatInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n info=\"The sampling temperature, between 0 and 1. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.\",\n advanced=True,\n value=0.0,\n ),\n ]\n outputs = [\n Output(\n display_name=\"Transcription\",\n name=\"transcription\",\n method=\"transcribe_audio\",\n ),\n ]\n\n def transcribe_audio(self) -> Message:\n client = Groq(api_key=self.groq_api_key)\n with open(self.audio_file, \"rb\") as file:\n transcription = client.audio.transcriptions.create(\n model=self.model_name,\n file=file,\n prompt=self.prompt or None,\n response_format=\"verbose_json\",\n temperature=self.temperature or 0.0,\n language=self.language or None\n )\n # print(transcription)\n return Message(text=transcription.text)","fileTypes":[],"file_path":"","password":false,"name":"code","advanced":true,"dynamic":true,"info":"","load_from_db":false,"title_case":false},"groq_api_key":{"load_from_db":true,"required":false,"placeholder":"","show":true,"name":"groq_api_key","value":"","display_name":"Groq API Key","advanced":false,"input_types":["Message"],"dynamic":false,"info":"The Groq API Key to use for the Whisper model.","title_case":false,"password":true,"type":"str","_input_type":"SecretStrInput"},"language":{"trace_as_metadata":true,"load_from_db":false,"list":false,"required":false,"placeholder":"","show":true,"name":"language","value":"","display_name":"Language","advanced":true,"dynamic":false,"info":"The language of the input audio. Supplying the input language in ISO-639-1 format will improve accuracy and latency.","title_case":false,"type":"str","_input_type":"StrInput"},"model_name":{"trace_as_metadata":true,"options":["distil-whisper-large-v3-en"],"combobox":false,"required":false,"placeholder":"","show":true,"name":"model_name","value":"distil-whisper-large-v3-en","display_name":"Model","advanced":false,"dynamic":false,"info":"The name of the model to use.","title_case":false,"type":"str","_input_type":"DropdownInput"},"prompt":{"trace_as_input":true,"trace_as_metadata":true,"load_from_db":false,"list":false,"required":false,"placeholder":"","show":true,"name":"prompt","value":"","display_name":"Prompt","advanced":true,"input_types":["Message"],"dynamic":false,"info":"An optional text to guide the model's style or continue a previous audio segment.","title_case":false,"type":"str","_input_type":"MessageTextInput"},"temperature":{"trace_as_metadata":true,"list":false,"required":false,"placeholder":"","show":true,"name":"temperature","value":0,"display_name":"Temperature","advanced":true,"dynamic":false,"info":"The sampling temperature, between 0 and 1. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.","title_case":false,"type":"float","_input_type":"FloatInput"}},"description":"Audio to text with Whisper model using Groq API","icon":"file-audio","base_classes":["Message"],"display_name":"Groq Whisper","documentation":"","custom_fields":{},"output_types":[],"pinned":false,"conditional_paths":[],"frozen":false,"outputs":[{"types":["Message"],"selected":"Message","name":"transcription","display_name":"Transcription","method":"transcribe_audio","value":"__UNDEFINED__","cache":true}],"field_order":["groq_api_key","audio_file","model_name","prompt","language","temperature"],"beta":false,"edited":true,"lf_version":"1.0.18"},"id":"GroqWhisperComponent-Lep46"},"selected":true,"width":384,"height":502,"dragging":false,"positionAbsolute":{"x":-1483.4857952490108,"y":37.275916175740406}},{"id":"Prompt-8rDKv","type":"genericNode","position":{"x":-981.6329126277515,"y":-67.39310132366732},"data":{"type":"Prompt","node":{"template":{"_type":"Component","code":{"type":"code","required":true,"placeholder":"","list":false,"show":true,"multiline":true,"value":"from langflow.base.prompts.api_utils import process_prompt_template\nfrom langflow.custom import Component\nfrom langflow.inputs.inputs import DefaultPromptField\nfrom langflow.io import Output, PromptInput\nfrom langflow.schema.message import Message\nfrom langflow.template.utils import update_template_values\n\n\nclass PromptComponent(Component):\n display_name: str = \"Prompt\"\n description: str = \"Create a prompt template with dynamic variables.\"\n icon = \"prompts\"\n trace_type = \"prompt\"\n name = \"Prompt\"\n\n inputs = [\n PromptInput(name=\"template\", display_name=\"Template\"),\n ]\n\n outputs = [\n Output(display_name=\"Prompt Message\", name=\"prompt\", method=\"build_prompt\"),\n ]\n\n async def build_prompt(\n self,\n ) -> Message:\n prompt = await Message.from_template_and_variables(**self._attributes)\n self.status = prompt.text\n return prompt\n\n def _update_template(self, frontend_node: dict):\n prompt_template = frontend_node[\"template\"][\"template\"][\"value\"]\n custom_fields = frontend_node[\"custom_fields\"]\n frontend_node_template = frontend_node[\"template\"]\n _ = process_prompt_template(\n template=prompt_template,\n name=\"template\",\n custom_fields=custom_fields,\n frontend_node_template=frontend_node_template,\n )\n return frontend_node\n\n def post_code_processing(self, new_frontend_node: dict, current_frontend_node: dict):\n \"\"\"\n This function is called after the code validation is done.\n \"\"\"\n frontend_node = super().post_code_processing(new_frontend_node, current_frontend_node)\n template = frontend_node[\"template\"][\"template\"][\"value\"]\n # Kept it duplicated for backwards compatibility\n _ = process_prompt_template(\n template=template,\n name=\"template\",\n custom_fields=frontend_node[\"custom_fields\"],\n frontend_node_template=frontend_node[\"template\"],\n )\n # Now that template is updated, we need to grab any values that were set in the current_frontend_node\n # and update the frontend_node with those values\n update_template_values(new_template=frontend_node, previous_template=current_frontend_node[\"template\"])\n return frontend_node\n\n def _get_fallback_input(self, **kwargs):\n return DefaultPromptField(**kwargs)\n","fileTypes":[],"file_path":"","password":false,"name":"code","advanced":true,"dynamic":true,"info":"","load_from_db":false,"title_case":false},"template":{"trace_as_input":true,"list":false,"required":false,"placeholder":"","show":true,"name":"template","value":"Based on the user transcription data you have access to, you need to produce a Breakdown of Categories:\n\nTasks: Tasks with varying priorities, owners, and due dates.\nExample task assignments include preparing reports, setting up meetings, and submitting proposals.\n\nDecisions: Important decisions made during the meeting\nDecisions include vendor choice, marketing strategy, and budget approval.\n\nQuestions: Questions raised during the meeting, with their status (answered/unanswered).\nAnswered questions include additional context in the form of answers.\n\nInsights: Insights based on the conversation, ranging from sales performance to concerns about deadlines.\nEach insight refer back to the exact part of the conversation.\n\nDeadlines: Upcoming deadlines related to the budget, product launch, and client presentation.\nThis helps track time-sensitive matters.\n\nAttendees: Attendees who attended the meeting\nThis tracks attendance and their respective roles.\n\nFollow-ups: Follow-up tasks assigned to individuals after the meeting, each with a due date.\nFollow-up items focus on clarifying budget, design, and scheduling next actions.\n\nRisks: Risks identified during the meeting, each with potential impacts on the project.\nThese include risks like budget overruns, delays, and potential staff turnover.\n\nAgenda: A list of the agenda items covered in the meeting.\nThe agenda provides a structured overview of the topics discussed. You need to extract as many items as you can, some might have 1-2 items, and some might 10, so make sure to capture every point.\n\nMeeting Name: The title of the meeting, reflecting its official designation. This gives a clear identifier for the meeting, often including a specific date or purpose, such as \"October 2024 Municipal Council Meeting.\"\n\nDescription: A high-level overview of the meeting’s purpose and key areas of focus. The description captures the essential topics discussed, decisions made, and the overall scope of the meeting, such as infrastructure updates, budget approvals, and key community concerns.\n\nSummary: A brief consolidation of the main points and outcomes from the meeting. The summary encapsulates the flow of the meeting, including major tasks, decisions, and action points, along with any significant challenges or risks highlighted, offering a concise review of the meeting’s results.\n\nYou must format your output as a JSON data, like:\n\n{output_example}\n\n\nThe transcript is as follow:\n\n{transcription}","display_name":"Template","advanced":false,"dynamic":false,"info":"","title_case":false,"type":"prompt","_input_type":"PromptInput"},"transcription":{"field_type":"str","required":false,"placeholder":"","list":false,"show":true,"multiline":true,"value":"","fileTypes":[],"file_path":"","name":"transcription","display_name":"transcription","advanced":false,"input_types":["Message","Text"],"dynamic":false,"info":"","load_from_db":false,"title_case":false,"type":"str"},"output_example":{"field_type":"str","required":false,"placeholder":"","list":false,"show":true,"multiline":true,"value":"","fileTypes":[],"file_path":"","name":"output_example","display_name":"output_example","advanced":false,"input_types":["Message","Text"],"dynamic":false,"info":"","load_from_db":false,"title_case":false,"type":"str"}},"description":"Create a prompt template with dynamic variables.","icon":"prompts","is_input":null,"is_output":null,"is_composition":null,"base_classes":["Message"],"name":"","display_name":"Prompt","documentation":"","custom_fields":{"template":["output_example","transcription"]},"output_types":[],"full_path":null,"pinned":false,"conditional_paths":[],"frozen":false,"outputs":[{"types":["Message"],"selected":"Message","name":"prompt","hidden":null,"display_name":"Prompt Message","method":"build_prompt","value":"__UNDEFINED__","cache":true}],"field_order":["template"],"beta":false,"error":null,"edited":false},"id":"Prompt-8rDKv"},"selected":false,"width":384,"height":502,"positionAbsolute":{"x":-981.6329126277515,"y":-67.39310132366732},"dragging":false},{"id":"TextInput-5MmdW","type":"genericNode","position":{"x":-1517.9689142084346,"y":-332.4509973473767},"data":{"type":"TextInput","node":{"template":{"_type":"Component","code":{"type":"code","required":true,"placeholder":"","list":false,"show":true,"multiline":true,"value":"from langflow.base.io.text import TextComponent\nfrom langflow.io import MultilineInput, Output\nfrom langflow.schema.message import Message\n\n\nclass TextInputComponent(TextComponent):\n display_name = \"Text Input\"\n description = \"Get text inputs from the Playground.\"\n icon = \"type\"\n name = \"TextInput\"\n\n inputs = [\n MultilineInput(\n name=\"input_value\",\n display_name=\"Text\",\n info=\"Text to be passed as input.\",\n ),\n ]\n outputs = [\n Output(display_name=\"Text\", name=\"text\", method=\"text_response\"),\n ]\n\n def text_response(self) -> Message:\n message = Message(\n text=self.input_value,\n )\n return message\n","fileTypes":[],"file_path":"","password":false,"name":"code","advanced":true,"dynamic":true,"info":"","load_from_db":false,"title_case":false},"input_value":{"trace_as_input":true,"multiline":true,"trace_as_metadata":true,"load_from_db":false,"list":false,"required":false,"placeholder":"","show":true,"name":"input_value","value":"{\n \"Breakdown\": {\n \"tasks\": [\n {\n \"description\": \"Prepare a report on the status of the procedural bylaw 2410.\",\n \"assigned_to\": \"CEO\",\n \"priority\": \"High\"\n },\n {\n \"description\": \"Follow up with the public works department regarding the tree asset projections.\",\n \"assigned_to\": \"Nomar\",\n \"priority\": \"Medium\"\n },\n {\n \"description\": \"Gather names for the two vacant positions on the Northeast Red Watershed District Committee.\",\n \"assigned_to\": \"Council Members\",\n \"priority\": \"Medium\"\n },\n {\n \"description\": \"Draft the policy changes for the new community grant system.\",\n \"assigned_to\": \"Grants Officer\",\n \"priority\": \"High\"\n },\n {\n \"description\": \"Schedule a meeting with local business leaders to discuss economic growth initiatives.\",\n \"assigned_to\": \"Economic Development Manager\",\n \"priority\": \"Low\"\n }\n ],\n \"decisions\": [\n {\n \"description\": \"The agenda for the meeting was approved unanimously.\"\n },\n {\n \"description\": \"The council activity reports for September were received as information.\"\n },\n {\n \"description\": \"The additional cost of $33,300 for the Settlers Road Bridge Crossing project will be included in the 2025 capital budget.\"\n },\n {\n \"description\": \"Nonprofit organizations and community service groups will receive grants for 2024 as listed.\"\n },\n {\n \"description\": \"The municipal recreation center expansion was approved with an amended budget.\"\n },\n {\n \"description\": \"The council decided to allocate $15,000 for the local library digital resources upgrade.\"\n }\n ],\n \"questions\": [\n {\n \"question\": \"What is the status of the procedural bylaw 2410?\",\n \"raised_by\": \"Janet\",\n \"status\": \"Unanswered\"\n },\n {\n \"question\": \"Why are the costs for the Settlers Road Bridge Crossing project increasing?\",\n \"raised_by\": \"Andy\",\n \"status\": \"Answered\",\n \"answer\": \"The costs are increasing due to unforeseen costs and additional decommissioning requirements for the existing infrastructure.\"\n },\n {\n \"question\": \"What is the Society of Ivan Franco?\",\n \"raised_by\": \"Mark\",\n \"status\": \"Answered\",\n \"answer\": \"It used to be an active community club located off Warren Hill Road, and discussions are ongoing about obtaining that land.\"\n },\n {\n \"question\": \"When will the public works department complete the tree asset projections?\",\n \"raised_by\": \"Councilor Miller\",\n \"status\": \"Pending\"\n },\n {\n \"question\": \"How is the council planning to address the growing number of emergency motor vehicle collisions?\",\n \"raised_by\": \"Glenn\",\n \"status\": \"Unanswered\"\n }\n ],\n \"insights\": [\n {\n \"description\": \"The council is committed to supporting seniors' activities and improving lodging for seniors in the community.\",\n },\n {\n \"description\": \"There is a need for a daycare center in the RM, and land has been identified for this purpose.\",\n },\n {\n \"description\": \"The increase in emergency motor vehicle collisions is concerning and needs further investigation.\",\n },\n {\n \"description\": \"Public interest in developing additional recreational trails continues to grow.\",\n },\n {\n \"description\": \"The community expressed concerns about rising utility rates in the region.\",\n }\n ],\n \"deadlines\": [\n {\n \"description\": \"Submit names for the two vacant positions on the Northeast Red Watershed District Committee.\",\n \"date\": \"2024-10-15\"\n },\n {\n \"description\": \"Prepare the report on the procedural bylaw 2410 for the next meeting.\",\n \"date\": \"2024-10-15\"\n },\n {\n \"description\": \"Submit the draft policy changes for the new community grant system.\",\n \"date\": \"2024-11-01\"\n },\n {\n \"description\": \"Submit the budget proposal for the library digital resources upgrade.\",\n \"date\": \"2024-10-20\"\n }\n ],\n \"attendees\": [\n {\n \"name\": \"Mr. Mayor\"\n },\n {\n \"name\": \"Councilor Miller\"\n },\n {\n \"name\": \"Councilor Fuels\"\n },\n {\n \"name\": \"Councilor Kaczynski\"\n },\n {\n \"name\": \"Councilor Warren\"\n },\n {\n \"name\": \"Councilor Lee\"\n },\n {\n \"name\": \"Mark\"\n },\n {\n \"name\": \"Melinda\"\n },\n {\n \"name\": \"Andy\"\n },\n {\n \"name\": \"Glenn\"\n },\n {\n \"name\": \"Janet\"\n },\n {\n \"name\": \"Nomar\"\n },\n {\n \"name\": \"Public Works Director\"\n },\n {\n \"name\": \"Grants Officer\"\n }\n ],\n \"follow_ups\": [\n {\n \"description\": \"Follow up with the finance team regarding the budget approval for the Settlers Road Bridge Crossing project.\",\n \"owner\": \"CEO\",\n \"due_date\": \"2024-10-18\"\n },\n {\n \"description\": \"Prepare a detailed report on the emergency motor vehicle collisions for the next meeting.\",\n \"owner\": \"Public Works Director\",\n \"due_date\": \"2024-10-15\"\n },\n {\n \"description\": \"Meet with the daycare development committee to review the proposed land options.\",\n \"owner\": \"Planning Department\",\n \"due_date\": \"2024-10-22\"\n },\n {\n \"description\": \"Organize a public forum on recreational trail development.\",\n \"owner\": \"Community Engagement Coordinator\",\n \"due_date\": \"2024-10-30\"\n }\n ],\n \"risks\": [\n {\n \"description\": \"There is a risk of budget overruns due to unforeseen costs in ongoing projects.\"\n },\n {\n \"description\": \"Potential delays in the Settlers Road Bridge Crossing project could impact future budgets.\"\n },\n {\n \"description\": \"A shortage of qualified contractors may delay the municipal recreation center expansion.\"\n },\n {\n \"description\": \"Uncertainty around the future of federal infrastructure funding could affect long-term projects.\"\n }\n ],\n \"agenda\": [\n \"Invocation and land acknowledgement\",\n \"Approval of the agenda\",\n \"Adoption of the minutes from the previous meeting\",\n \"Reports from council activities\",\n \"Departmental reports\",\n \"Question period\",\n \"Consent agenda\",\n \"Settlers Road Bridge Crossing project discussion\",\n \"Northeast Red Watershed District Committee appointments\",\n \"2024 nonprofit community grants discussion\",\n \"Public forum planning for recreational trail development\",\n \"2025 capital budget planning\",\n \"Utility rate increase concerns\",\n \"Closing of the meeting\"\n ],\n \"meeting_name\": \"October 2024 Municipal Council Meeting\",\n \"description\": \"This meeting covered several key topics including updates on ongoing infrastructure projects, community grant allocations, and recreational development initiatives. Key decisions were made regarding the Settlers Road Bridge Crossing, the municipal recreation center expansion, and the allocation of funds for the local library's digital resources. Questions and concerns about rising utility rates and the growing number of emergency motor vehicle collisions were raised. Several tasks, follow-ups, and deadlines were established to address ongoing issues, and risks were identified for future project planning.\",\n \"summary\": \"The meeting involved high-priority tasks such as preparing a procedural bylaw report, addressing public works follow-ups, and drafting policy changes for the community grant system. Key decisions included funding allocations for infrastructure projects and community services. Several questions were raised about project cost increases and emergency incidents, while the council focused on issues like recreational trail development and senior support. Insights indicated growing public concerns over utility rates, and various risks to project budgets and timelines were discussed.\"\n }\n}\n\n","display_name":"Text","advanced":false,"input_types":["Message"],"dynamic":false,"info":"Text to be passed as input.","title_case":false,"type":"str","_input_type":"MultilineInput"}},"description":"Get text inputs from the Playground.","icon":"type","base_classes":["Message"],"display_name":"Format JSON Template of Tasks","documentation":"","custom_fields":{},"output_types":[],"pinned":false,"conditional_paths":[],"frozen":false,"outputs":[{"types":["Message"],"selected":"Message","name":"text","display_name":"Text","method":"text_response","value":"__UNDEFINED__","cache":true}],"field_order":["input_value"],"beta":false,"edited":false,"lf_version":"1.0.18"},"id":"TextInput-5MmdW"},"selected":false,"width":384,"height":302,"positionAbsolute":{"x":-1517.9689142084346,"y":-332.4509973473767},"dragging":false},{"id":"OpenAIModel-iM892","type":"genericNode","position":{"x":-502.26482219657385,"y":369.9358469220685},"data":{"type":"OpenAIModel","node":{"template":{"_type":"Component","api_key":{"load_from_db":true,"required":false,"placeholder":"","show":true,"name":"api_key","value":"","display_name":"OpenAI API Key","advanced":false,"input_types":["Message"],"dynamic":false,"info":"The OpenAI API Key to use for the OpenAI model.","title_case":false,"password":true,"type":"str","_input_type":"SecretStrInput"},"code":{"type":"code","required":true,"placeholder":"","list":false,"show":true,"multiline":true,"value":"import operator\nfrom functools import reduce\n\nfrom langflow.field_typing.range_spec import RangeSpec\nfrom langchain_openai import ChatOpenAI\nfrom pydantic.v1 import SecretStr\n\nfrom langflow.base.models.model import LCModelComponent\nfrom langflow.base.models.openai_constants import OPENAI_MODEL_NAMES\nfrom langflow.field_typing import LanguageModel\nfrom langflow.inputs import (\n BoolInput,\n DictInput,\n DropdownInput,\n FloatInput,\n IntInput,\n SecretStrInput,\n StrInput,\n)\n\n\nclass OpenAIModelComponent(LCModelComponent):\n display_name = \"OpenAI\"\n description = \"Generates text using OpenAI LLMs.\"\n icon = \"OpenAI\"\n name = \"OpenAIModel\"\n\n inputs = LCModelComponent._base_inputs + [\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n advanced=True,\n info=\"The maximum number of tokens to generate. Set to 0 for unlimited tokens.\",\n range_spec=RangeSpec(min=0, max=128000),\n ),\n DictInput(name=\"model_kwargs\", display_name=\"Model Kwargs\", advanced=True),\n BoolInput(\n name=\"json_mode\",\n display_name=\"JSON Mode\",\n advanced=True,\n info=\"If True, it will output JSON regardless of passing a schema.\",\n ),\n DictInput(\n name=\"output_schema\",\n is_list=True,\n display_name=\"Schema\",\n advanced=True,\n info=\"The schema for the Output of the model. You must pass the word JSON in the prompt. If left blank, JSON mode will be disabled.\",\n ),\n DropdownInput(\n name=\"model_name\",\n display_name=\"Model Name\",\n advanced=False,\n options=OPENAI_MODEL_NAMES,\n value=OPENAI_MODEL_NAMES[0],\n ),\n StrInput(\n name=\"openai_api_base\",\n display_name=\"OpenAI API Base\",\n advanced=True,\n info=\"The base URL of the OpenAI API. Defaults to https://api.openai.com/v1. You can change this to use other APIs like JinaChat, LocalAI and Prem.\",\n ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"OpenAI API Key\",\n info=\"The OpenAI API Key to use for the OpenAI model.\",\n advanced=False,\n value=\"OPENAI_API_KEY\",\n ),\n FloatInput(name=\"temperature\", display_name=\"Temperature\", value=0.1),\n IntInput(\n name=\"seed\",\n display_name=\"Seed\",\n info=\"The seed controls the reproducibility of the job.\",\n advanced=True,\n value=1,\n ),\n ]\n\n def build_model(self) -> LanguageModel: # type: ignore[type-var]\n # self.output_schema is a list of dictionaries\n # let's convert it to a dictionary\n output_schema_dict: dict[str, str] = reduce(operator.ior, self.output_schema or {}, {})\n openai_api_key = self.api_key\n temperature = self.temperature\n model_name: str = self.model_name\n max_tokens = self.max_tokens\n model_kwargs = self.model_kwargs or {}\n openai_api_base = self.openai_api_base or \"https://api.openai.com/v1\"\n json_mode = bool(output_schema_dict) or self.json_mode\n seed = self.seed\n\n if openai_api_key:\n api_key = SecretStr(openai_api_key)\n else:\n api_key = None\n output = ChatOpenAI(\n max_tokens=max_tokens or None,\n model_kwargs=model_kwargs,\n model=model_name,\n base_url=openai_api_base,\n api_key=api_key,\n temperature=temperature if temperature is not None else 0.1,\n seed=seed,\n )\n if json_mode:\n if output_schema_dict:\n output = output.with_structured_output(schema=output_schema_dict, method=\"json_mode\") # type: ignore\n else:\n output = output.bind(response_format={\"type\": \"json_object\"}) # type: ignore\n\n return output # type: ignore\n\n def _get_exception_message(self, e: Exception):\n \"\"\"\n Get a message from an OpenAI exception.\n\n Args:\n exception (Exception): The exception to get the message from.\n\n Returns:\n str: The message from the exception.\n \"\"\"\n\n try:\n from openai import BadRequestError\n except ImportError:\n return\n if isinstance(e, BadRequestError):\n message = e.body.get(\"message\") # type: ignore\n if message:\n return message\n return\n","fileTypes":[],"file_path":"","password":false,"name":"code","advanced":true,"dynamic":true,"info":"","load_from_db":false,"title_case":false},"input_value":{"trace_as_input":true,"trace_as_metadata":true,"load_from_db":false,"list":false,"required":false,"placeholder":"","show":true,"name":"input_value","value":"","display_name":"Input","advanced":false,"input_types":["Message"],"dynamic":false,"info":"","title_case":false,"type":"str","_input_type":"MessageInput"},"json_mode":{"trace_as_metadata":true,"list":false,"required":false,"placeholder":"","show":true,"name":"json_mode","value":true,"display_name":"JSON Mode","advanced":false,"dynamic":false,"info":"If True, it will output JSON regardless of passing a schema.","title_case":false,"type":"bool","_input_type":"BoolInput"},"max_tokens":{"trace_as_metadata":true,"range_spec":{"step_type":"float","min":0,"max":128000,"step":0.1},"list":false,"required":false,"placeholder":"","show":true,"name":"max_tokens","value":"","display_name":"Max Tokens","advanced":true,"dynamic":false,"info":"The maximum number of tokens to generate. Set to 0 for unlimited tokens.","title_case":false,"type":"int","_input_type":"IntInput"},"model_kwargs":{"trace_as_input":true,"list":false,"required":false,"placeholder":"","show":true,"name":"model_kwargs","value":{},"display_name":"Model Kwargs","advanced":true,"dynamic":false,"info":"","title_case":false,"type":"dict","_input_type":"DictInput"},"model_name":{"trace_as_metadata":true,"options":["gpt-4o-mini","gpt-4o","gpt-4-turbo","gpt-4-turbo-preview","gpt-4","gpt-3.5-turbo","gpt-3.5-turbo-0125"],"combobox":false,"required":false,"placeholder":"","show":true,"name":"model_name","value":"gpt-4o-mini","display_name":"Model Name","advanced":false,"dynamic":false,"info":"","title_case":false,"type":"str","_input_type":"DropdownInput"},"openai_api_base":{"trace_as_metadata":true,"load_from_db":false,"list":false,"required":false,"placeholder":"","show":true,"name":"openai_api_base","value":"","display_name":"OpenAI API Base","advanced":true,"dynamic":false,"info":"The base URL of the OpenAI API. Defaults to https://api.openai.com/v1. You can change this to use other APIs like JinaChat, LocalAI and Prem.","title_case":false,"type":"str","_input_type":"StrInput"},"output_schema":{"trace_as_input":true,"list":true,"required":false,"placeholder":"","show":true,"name":"output_schema","value":{},"display_name":"Schema","advanced":true,"dynamic":false,"info":"The schema for the Output of the model. You must pass the word JSON in the prompt. If left blank, JSON mode will be disabled.","title_case":false,"type":"dict","_input_type":"DictInput"},"seed":{"trace_as_metadata":true,"list":false,"required":false,"placeholder":"","show":true,"name":"seed","value":1,"display_name":"Seed","advanced":true,"dynamic":false,"info":"The seed controls the reproducibility of the job.","title_case":false,"type":"int","_input_type":"IntInput"},"stream":{"trace_as_metadata":true,"list":false,"required":false,"placeholder":"","show":true,"name":"stream","value":false,"display_name":"Stream","advanced":true,"dynamic":false,"info":"Stream the response from the model. Streaming works only in Chat.","title_case":false,"type":"bool","_input_type":"BoolInput"},"system_message":{"trace_as_input":true,"trace_as_metadata":true,"load_from_db":false,"list":false,"required":false,"placeholder":"","show":true,"name":"system_message","value":"","display_name":"System Message","advanced":true,"input_types":["Message"],"dynamic":false,"info":"System message to pass to the model.","title_case":false,"type":"str","_input_type":"MessageTextInput"},"temperature":{"trace_as_metadata":true,"list":false,"required":false,"placeholder":"","show":true,"name":"temperature","value":0.1,"display_name":"Temperature","advanced":false,"dynamic":false,"info":"","title_case":false,"type":"float","_input_type":"FloatInput"}},"description":"Generates text using OpenAI LLMs.","icon":"OpenAI","base_classes":["LanguageModel","Message"],"display_name":"OpenAI","documentation":"","custom_fields":{},"output_types":[],"pinned":false,"conditional_paths":[],"frozen":false,"outputs":[{"types":["Message"],"selected":"Message","name":"text_output","display_name":"Text","method":"text_response","value":"__UNDEFINED__","cache":true},{"types":["LanguageModel"],"selected":"LanguageModel","name":"model_output","display_name":"Language Model","method":"build_model","value":"__UNDEFINED__","cache":true}],"field_order":["input_value","system_message","stream","max_tokens","model_kwargs","json_mode","output_schema","model_name","openai_api_base","api_key","temperature","seed"],"beta":false,"edited":false,"lf_version":"1.0.18"},"id":"OpenAIModel-iM892"},"selected":false,"width":384,"height":677,"positionAbsolute":{"x":-502.26482219657385,"y":369.9358469220685},"dragging":false},{"id":"JSONCleaner-dSEIi","type":"genericNode","position":{"x":93.51863880601047,"y":428.6163565103991},"data":{"type":"JSONCleaner","node":{"template":{"_type":"Component","code":{"type":"code","required":true,"placeholder":"","list":false,"show":true,"multiline":true,"value":"import json\nimport re\nimport unicodedata\nfrom langflow.custom import Component\nfrom langflow.inputs import MessageTextInput, BoolInput\nfrom langflow.template import Output\nfrom langflow.schema.message import Message\n\n\nclass JSONCleaner(Component):\n display_name = \"JSON Cleaner\"\n description = \"Cleans the messy and sometimes incorrect JSON strings produced by LLMs so that they are fully compliant with the JSON spec.\"\n icon = \"custom_components\"\n\n inputs = [\n MessageTextInput(\n name=\"json_str\", display_name=\"JSON String\", info=\"The JSON string to be cleaned.\", required=True\n ),\n BoolInput(\n name=\"remove_control_chars\",\n display_name=\"Remove Control Characters\",\n info=\"Remove control characters from the JSON string.\",\n required=False,\n ),\n BoolInput(\n name=\"normalize_unicode\",\n display_name=\"Normalize Unicode\",\n info=\"Normalize Unicode characters in the JSON string.\",\n required=False,\n ),\n BoolInput(\n name=\"validate_json\",\n display_name=\"Validate JSON\",\n info=\"Validate the JSON string to ensure it is well-formed.\",\n required=False,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Cleaned JSON String\", name=\"output\", method=\"clean_json\"),\n ]\n\n def clean_json(self) -> Message:\n try:\n from json_repair import repair_json # type: ignore\n except ImportError:\n raise ImportError(\n \"Could not import the json_repair package.\" \"Please install it with `pip install json_repair`.\"\n )\n\n \"\"\"Clean the input JSON string based on provided options and return the cleaned JSON string.\"\"\"\n json_str = self.json_str\n remove_control_chars = self.remove_control_chars\n normalize_unicode = self.normalize_unicode\n validate_json = self.validate_json\n\n try:\n start = json_str.find(\"{\")\n end = json_str.rfind(\"}\")\n if start == -1 or end == -1:\n raise ValueError(\"Invalid JSON string: Missing '{' or '}'\")\n json_str = json_str[start : end + 1]\n\n if remove_control_chars:\n json_str = self._remove_control_characters(json_str)\n if normalize_unicode:\n json_str = self._normalize_unicode(json_str)\n if validate_json:\n json_str = self._validate_json(json_str)\n\n cleaned_json_str = repair_json(json_str)\n result = str(cleaned_json_str)\n\n self.status = result\n return Message(text=result)\n except Exception as e:\n raise ValueError(f\"Error cleaning JSON string: {str(e)}\")\n\n def _remove_control_characters(self, s: str) -> str:\n \"\"\"Remove control characters from the string.\"\"\"\n return re.sub(r\"[\\x00-\\x1F\\x7F]\", \"\", s)\n\n def _normalize_unicode(self, s: str) -> str:\n \"\"\"Normalize Unicode characters in the string.\"\"\"\n return unicodedata.normalize(\"NFC\", s)\n\n def _validate_json(self, s: str) -> str:\n \"\"\"Validate the JSON string.\"\"\"\n try:\n json.loads(s)\n return s\n except json.JSONDecodeError as e:\n raise ValueError(f\"Invalid JSON string: {str(e)}\")\n","fileTypes":[],"file_path":"","password":false,"name":"code","advanced":true,"dynamic":true,"info":"","load_from_db":false,"title_case":false},"json_str":{"trace_as_input":true,"trace_as_metadata":true,"load_from_db":false,"list":false,"required":true,"placeholder":"","show":true,"name":"json_str","value":"","display_name":"JSON String","advanced":false,"input_types":["Message"],"dynamic":false,"info":"The JSON string to be cleaned.","title_case":false,"type":"str","_input_type":"MessageTextInput"},"normalize_unicode":{"trace_as_metadata":true,"list":false,"required":false,"placeholder":"","show":true,"name":"normalize_unicode","value":false,"display_name":"Normalize Unicode","advanced":false,"dynamic":false,"info":"Normalize Unicode characters in the JSON string.","title_case":false,"type":"bool","_input_type":"BoolInput"},"remove_control_chars":{"trace_as_metadata":true,"list":false,"required":false,"placeholder":"","show":true,"name":"remove_control_chars","value":false,"display_name":"Remove Control Characters","advanced":false,"dynamic":false,"info":"Remove control characters from the JSON string.","title_case":false,"type":"bool","_input_type":"BoolInput"},"validate_json":{"trace_as_metadata":true,"list":false,"required":false,"placeholder":"","show":true,"name":"validate_json","value":true,"display_name":"Validate JSON","advanced":false,"dynamic":false,"info":"Validate the JSON string to ensure it is well-formed.","title_case":false,"type":"bool","_input_type":"BoolInput"}},"description":"Cleans the messy and sometimes incorrect JSON strings produced by LLMs so that they are fully compliant with the JSON spec.","icon":"custom_components","base_classes":["Message"],"display_name":"JSON Cleaner","documentation":"","custom_fields":{},"output_types":[],"pinned":false,"conditional_paths":[],"frozen":false,"outputs":[{"types":["Message"],"selected":"Message","name":"output","display_name":"Cleaned JSON String","method":"clean_json","value":"__UNDEFINED__","cache":true}],"field_order":["json_str","remove_control_chars","normalize_unicode","validate_json"],"beta":false,"edited":false,"lf_version":"1.0.18"},"id":"JSONCleaner-dSEIi"},"selected":false,"width":384,"height":574,"positionAbsolute":{"x":93.51863880601047,"y":428.6163565103991},"dragging":false},{"id":"TextOutput-By44u","type":"genericNode","position":{"x":730.3183362631719,"y":684.803464364877},"data":{"type":"TextOutput","node":{"template":{"_type":"Component","code":{"type":"code","required":true,"placeholder":"","list":false,"show":true,"multiline":true,"value":"from langflow.base.io.text import TextComponent\nfrom langflow.io import MultilineInput, Output\nfrom langflow.schema.message import Message\n\n\nclass TextOutputComponent(TextComponent):\n display_name = \"Text Output\"\n description = \"Display a text output in the Playground.\"\n icon = \"type\"\n name = \"TextOutput\"\n\n inputs = [\n MultilineInput(\n name=\"input_value\",\n display_name=\"Text\",\n info=\"Text to be passed as output.\",\n ),\n ]\n outputs = [\n Output(display_name=\"Text\", name=\"breakdown\", method=\"text_response\"),\n ]\n\n def text_response(self) -> Message:\n message = Message(\n text=self.input_value,\n )\n self.status = self.input_value\n return message\n","fileTypes":[],"file_path":"","password":false,"name":"code","advanced":true,"dynamic":true,"info":"","load_from_db":false,"title_case":false},"input_value":{"trace_as_input":true,"multiline":true,"trace_as_metadata":true,"load_from_db":false,"list":false,"required":false,"placeholder":"","show":true,"name":"input_value","value":"","display_name":"Text","advanced":false,"input_types":["Message"],"dynamic":false,"info":"Text to be passed as output.","title_case":false,"type":"str","_input_type":"MultilineInput"}},"description":"Display a text output in the Playground.","icon":"type","base_classes":["Message"],"display_name":"Text Output","documentation":"","custom_fields":{},"output_types":[],"pinned":false,"conditional_paths":[],"frozen":false,"outputs":[{"types":["Message"],"selected":"Message","name":"breakdown","display_name":"Text","method":"text_response","value":"__UNDEFINED__","cache":true}],"field_order":["input_value"],"beta":false,"edited":true},"id":"TextOutput-By44u"},"selected":false,"width":384,"height":302,"positionAbsolute":{"x":730.3183362631719,"y":684.803464364877},"dragging":false},{"id":"TextOutput-V5ldV","type":"genericNode","position":{"x":-227.37117065217348,"y":1341.931192742318},"data":{"type":"TextOutput","node":{"template":{"_type":"Component","code":{"type":"code","required":true,"placeholder":"","list":false,"show":true,"multiline":true,"value":"from langflow.base.io.text import TextComponent\nfrom langflow.io import MultilineInput, Output\nfrom langflow.schema.message import Message\n\n\nclass TextOutputComponent(TextComponent):\n display_name = \"Text Output\"\n description = \"Display a text output in the Playground.\"\n icon = \"type\"\n name = \"TextOutput\"\n\n inputs = [\n MultilineInput(\n name=\"input_value\",\n display_name=\"Text\",\n info=\"Text to be passed as output.\",\n ),\n ]\n outputs = [\n Output(display_name=\"Text\", name=\"transcription\", method=\"text_response\"),\n ]\n\n def text_response(self) -> Message:\n message = Message(\n text=self.input_value,\n )\n self.status = self.input_value\n return message\n","fileTypes":[],"file_path":"","password":false,"name":"code","advanced":true,"dynamic":true,"info":"","load_from_db":false,"title_case":false},"input_value":{"trace_as_input":true,"multiline":true,"trace_as_metadata":true,"load_from_db":false,"list":false,"required":false,"placeholder":"","show":true,"name":"input_value","value":"","display_name":"Text","advanced":false,"input_types":["Message"],"dynamic":false,"info":"Text to be passed as output.","title_case":false,"type":"str","_input_type":"MultilineInput"}},"description":"Display a text output in the Playground.","icon":"type","base_classes":["Message"],"display_name":"Text Output","documentation":"","custom_fields":{},"output_types":[],"pinned":false,"conditional_paths":[],"frozen":false,"outputs":[{"types":["Message"],"selected":"Message","name":"transcription","display_name":"Text","method":"text_response","value":"__UNDEFINED__","cache":true}],"field_order":["input_value"],"beta":false,"edited":true},"id":"TextOutput-V5ldV"},"selected":false,"width":384,"height":302,"positionAbsolute":{"x":-227.37117065217348,"y":1341.931192742318},"dragging":false}],"edges":[{"source":"TextInput-5MmdW","sourceHandle":"{œdataTypeœ:œTextInputœ,œidœ:œTextInput-5MmdWœ,œnameœ:œtextœ,œoutput_typesœ:[œMessageœ]}","target":"Prompt-8rDKv","targetHandle":"{œfieldNameœ:œoutput_exampleœ,œidœ:œPrompt-8rDKvœ,œinputTypesœ:[œMessageœ,œTextœ],œtypeœ:œstrœ}","data":{"targetHandle":{"fieldName":"output_example","id":"Prompt-8rDKv","inputTypes":["Message","Text"],"type":"str"},"sourceHandle":{"dataType":"TextInput","id":"TextInput-5MmdW","name":"text","output_types":["Message"]}},"id":"reactflow__edge-TextInput-5MmdW{œdataTypeœ:œTextInputœ,œidœ:œTextInput-5MmdWœ,œnameœ:œtextœ,œoutput_typesœ:[œMessageœ]}-Prompt-8rDKv{œfieldNameœ:œoutput_exampleœ,œidœ:œPrompt-8rDKvœ,œinputTypesœ:[œMessageœ,œTextœ],œtypeœ:œstrœ}","className":""},{"source":"GroqWhisperComponent-Lep46","sourceHandle":"{œdataTypeœ:œGroqWhisperComponentœ,œidœ:œGroqWhisperComponent-Lep46œ,œnameœ:œtranscriptionœ,œoutput_typesœ:[œMessageœ]}","target":"Prompt-8rDKv","targetHandle":"{œfieldNameœ:œtranscriptionœ,œidœ:œPrompt-8rDKvœ,œinputTypesœ:[œMessageœ,œTextœ],œtypeœ:œstrœ}","data":{"targetHandle":{"fieldName":"transcription","id":"Prompt-8rDKv","inputTypes":["Message","Text"],"type":"str"},"sourceHandle":{"dataType":"GroqWhisperComponent","id":"GroqWhisperComponent-Lep46","name":"transcription","output_types":["Message"]}},"id":"reactflow__edge-GroqWhisperComponent-Lep46{œdataTypeœ:œGroqWhisperComponentœ,œidœ:œGroqWhisperComponent-Lep46œ,œnameœ:œtranscriptionœ,œoutput_typesœ:[œMessageœ]}-Prompt-8rDKv{œfieldNameœ:œtranscriptionœ,œidœ:œPrompt-8rDKvœ,œinputTypesœ:[œMessageœ,œTextœ],œtypeœ:œstrœ}","className":""},{"source":"Prompt-8rDKv","sourceHandle":"{œdataTypeœ:œPromptœ,œidœ:œPrompt-8rDKvœ,œnameœ:œpromptœ,œoutput_typesœ:[œMessageœ]}","target":"OpenAIModel-iM892","targetHandle":"{œfieldNameœ:œinput_valueœ,œidœ:œOpenAIModel-iM892œ,œinputTypesœ:[œMessageœ],œtypeœ:œstrœ}","data":{"targetHandle":{"fieldName":"input_value","id":"OpenAIModel-iM892","inputTypes":["Message"],"type":"str"},"sourceHandle":{"dataType":"Prompt","id":"Prompt-8rDKv","name":"prompt","output_types":["Message"]}},"id":"reactflow__edge-Prompt-8rDKv{œdataTypeœ:œPromptœ,œidœ:œPrompt-8rDKvœ,œnameœ:œpromptœ,œoutput_typesœ:[œMessageœ]}-OpenAIModel-iM892{œfieldNameœ:œinput_valueœ,œidœ:œOpenAIModel-iM892œ,œinputTypesœ:[œMessageœ],œtypeœ:œstrœ}","className":""},{"source":"OpenAIModel-iM892","sourceHandle":"{œdataTypeœ:œOpenAIModelœ,œidœ:œOpenAIModel-iM892œ,œnameœ:œtext_outputœ,œoutput_typesœ:[œMessageœ]}","target":"JSONCleaner-dSEIi","targetHandle":"{œfieldNameœ:œjson_strœ,œidœ:œJSONCleaner-dSEIiœ,œinputTypesœ:[œMessageœ],œtypeœ:œstrœ}","data":{"targetHandle":{"fieldName":"json_str","id":"JSONCleaner-dSEIi","inputTypes":["Message"],"type":"str"},"sourceHandle":{"dataType":"OpenAIModel","id":"OpenAIModel-iM892","name":"text_output","output_types":["Message"]}},"id":"reactflow__edge-OpenAIModel-iM892{œdataTypeœ:œOpenAIModelœ,œidœ:œOpenAIModel-iM892œ,œnameœ:œtext_outputœ,œoutput_typesœ:[œMessageœ]}-JSONCleaner-dSEIi{œfieldNameœ:œjson_strœ,œidœ:œJSONCleaner-dSEIiœ,œinputTypesœ:[œMessageœ],œtypeœ:œstrœ}","className":""},{"source":"JSONCleaner-dSEIi","sourceHandle":"{œdataTypeœ:œJSONCleanerœ,œidœ:œJSONCleaner-dSEIiœ,œnameœ:œoutputœ,œoutput_typesœ:[œMessageœ]}","target":"TextOutput-By44u","targetHandle":"{œfieldNameœ:œinput_valueœ,œidœ:œTextOutput-By44uœ,œinputTypesœ:[œMessageœ],œtypeœ:œstrœ}","data":{"targetHandle":{"fieldName":"input_value","id":"TextOutput-By44u","inputTypes":["Message"],"type":"str"},"sourceHandle":{"dataType":"JSONCleaner","id":"JSONCleaner-dSEIi","name":"output","output_types":["Message"]}},"id":"reactflow__edge-JSONCleaner-dSEIi{œdataTypeœ:œJSONCleanerœ,œidœ:œJSONCleaner-dSEIiœ,œnameœ:œoutputœ,œoutput_typesœ:[œMessageœ]}-TextOutput-By44u{œfieldNameœ:œinput_valueœ,œidœ:œTextOutput-By44uœ,œinputTypesœ:[œMessageœ],œtypeœ:œstrœ}","className":""},{"source":"GroqWhisperComponent-Lep46","sourceHandle":"{œdataTypeœ:œGroqWhisperComponentœ,œidœ:œGroqWhisperComponent-Lep46œ,œnameœ:œtranscriptionœ,œoutput_typesœ:[œMessageœ]}","target":"TextOutput-V5ldV","targetHandle":"{œfieldNameœ:œinput_valueœ,œidœ:œTextOutput-V5ldVœ,œinputTypesœ:[œMessageœ],œtypeœ:œstrœ}","data":{"targetHandle":{"fieldName":"input_value","id":"TextOutput-V5ldV","inputTypes":["Message"],"type":"str"},"sourceHandle":{"dataType":"GroqWhisperComponent","id":"GroqWhisperComponent-Lep46","name":"transcription","output_types":["Message"]}},"id":"reactflow__edge-GroqWhisperComponent-Lep46{œdataTypeœ:œGroqWhisperComponentœ,œidœ:œGroqWhisperComponent-Lep46œ,œnameœ:œtranscriptionœ,œoutput_typesœ:[œMessageœ]}-TextOutput-V5ldV{œfieldNameœ:œinput_valueœ,œidœ:œTextOutput-V5ldVœ,œinputTypesœ:[œMessageœ],œtypeœ:œstrœ}","className":""}],"viewport":{"x":1475.93280164109,"y":49.891604442645985,"zoom":0.8393642050674969}},"description":"A Flow that processes an audio using Groq Whisper, process it and return a JSON of the analysis.","name":"Meeting Mind","last_tested_version":"1.0.18","endpoint_name":null,"is_component":false} --------------------------------------------------------------------------------