├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── app ├── api │ ├── chat │ │ └── route.ts │ ├── followUp │ │ └── route.ts │ ├── generateDetails │ │ └── route.ts │ ├── generateGraph │ │ └── route.ts │ ├── generateSearchQueries │ │ └── route.ts │ ├── generateSpreadsheetColumns │ │ └── route.ts │ ├── researchDetails │ │ └── route.ts │ └── researchIdea │ │ └── route.ts ├── auth │ └── signin │ │ └── page.tsx ├── demo │ └── page.tsx ├── favicon.ico ├── fonts │ ├── GeistMonoVF.woff │ └── GeistVF.woff ├── globals.css ├── layout.tsx ├── page.tsx └── providers.tsx ├── components.json ├── components ├── AreaSelection.tsx ├── ButtonLoading.tsx ├── ChatbotNode.tsx ├── ContextDialog.tsx ├── ContextNode.tsx ├── DockDemo.tsx ├── ExpandableNode.tsx ├── FlowCanvas.tsx ├── ModernChatbot.tsx ├── SearchBox.tsx ├── SidePanel.tsx ├── SignInForm.tsx ├── SpreadsheetDialog.tsx ├── SpreadsheetNode.tsx ├── multi-select.tsx └── ui │ ├── avatar.tsx │ ├── badge.tsx │ ├── button.tsx │ ├── card.tsx │ ├── checkbox.tsx │ ├── command.tsx │ ├── context-menu.tsx │ ├── dialog.tsx │ ├── dock.tsx │ ├── dot-pattern.tsx │ ├── input.tsx │ ├── label.tsx │ ├── popover.tsx │ ├── progress.tsx │ ├── scroll-area.tsx │ ├── select.tsx │ ├── separator.tsx │ ├── sheet.tsx │ ├── tabs.tsx │ ├── textarea.tsx │ └── tooltip.tsx ├── lib ├── firebase.ts └── utils.ts ├── next.config.ts ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public ├── file.svg ├── globe.svg ├── next.svg ├── vercel.svg └── window.svg ├── storage └── store.ts ├── tailwind.config.ts ├── tsconfig.json └── utils ├── agentExecutor.ts └── cleanData.js /.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.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | # env files (can opt-in for commiting if needed) 33 | .env* 34 | 35 | # vercel 36 | .vercel 37 | 38 | # typescript 39 | *.tsbuildinfo 40 | next-env.d.ts 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Shoib Loya 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 | # 🚀 Nuggt: Go from 0 to 1 on any topic instantly 2 | 3 | Join Discord: [Discord](https://discord.gg/JGF6f448) 4 | 5 | 🎥 **Demo** 6 | 7 | 8 | 9 | https://github.com/user-attachments/assets/78b35459-c2c1-4eab-ae7c-0c60637b79fe 10 | 11 | 12 | 13 | --- 14 | 15 | ## 🌟 Features 16 | 17 | - **🔍 Search & Research** 18 | Gather info on any topic from the internet with the search feature. 19 | Example: Search "How to make a good elevator pitch" and explore a detailed research tree. 20 | 21 | - **📚 Context Creation** 22 | Save important info to reusable contexts by highlighting and adding to the console. Use these to enrich chatbot conversations. 23 | 24 | - **🤖 Chatbot with Contexts** 25 | Select a context to include in your conversations with the AI. Makes responses smarter and more relevant! 26 | 27 | - **✨ Beautify Graphs** 28 | Auto-arrange your research nodes with a single click for easy navigation. 29 | 30 | - **📊 Spreadsheet (Coming Soon)** 31 | Manage your data better with this upcoming feature! 32 | 33 | --- 34 | 35 | ## 🛠️ Setup 36 | 37 | 1. **Clone the Repo** 38 | ```bash 39 | git clone https://github.com/shoibloya/nuggt-research.git 40 | cd nuggt-research 41 | ``` 42 | 43 | 2. **Create a `.env` File** 44 | In the project root, create a `.env` file and add the following keys: 45 | ```env 46 | OPENAI_API_KEY="your-openai-api-key" 47 | TAVILY_API_KEY="your-tavily-api-key" 48 | FIRECRAWL_API_KEY="your-firecrawl-api-key" 49 | ``` 50 | 51 | **Optional (for login & Firebase storage):** 52 | Add these keys only if you want to log in and save data to Firebase: 53 | ```env 54 | NEXTAUTH_SECRET="your-nextauth-secret" 55 | NEXT_PUBLIC_FIREBASE_API_KEY="your-firebase-api-key" 56 | NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN="your-firebase-auth-domain" 57 | NEXT_PUBLIC_FIREBASE_PROJECT_ID="your-firebase-project-id" 58 | NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET="your-firebase-storage-bucket" 59 | NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID="your-firebase-messaging-sender-id" 60 | NEXT_PUBLIC_FIREBASE_APP_ID="your-firebase-app-id" 61 | NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID="your-firebase-measurement-id" 62 | ``` 63 | 64 | 3. **Install Dependencies** 65 | ```bash 66 | npm install 67 | ``` 68 | 69 | 4. **Run the App** 70 | ```bash 71 | npm run dev 72 | ``` 73 | Open [http://localhost:3000](http://localhost:3000) in your browser. 74 | 75 | --- 76 | 77 | Happy exploring! 🌟 78 | -------------------------------------------------------------------------------- /app/api/chat/route.ts: -------------------------------------------------------------------------------- 1 | // app/api/chat/route.ts 2 | 3 | import { NextResponse } from 'next/server'; 4 | import OpenAI from 'openai'; 5 | 6 | const openai = new OpenAI({ 7 | apiKey: process.env.OPENAI_API_KEY, // Ensure your API key is set in environment variables 8 | }); 9 | 10 | const SYSTEM_MESSAGE = { 11 | role: 'system', 12 | content: `You are an expert but you are also the user's best friend. If the user comes to you with a task, your job is not to complete the task by yourself but to work with the user as a team. For any task, the first step is always to break it down into smaller subtasks and complete them one by one. Since you are a team player, you do not move on to the next task until you and the user agree with the work done for the current subtask. Your approach is very simple and highly creative. 13 | The first thing you do for any subtask, is to look at the context and brainstorm different ways you can approach the subtask. Your brainstorming process is very natural and its like a conversation as if you are talking to yourself. As you analyse and draw creative connections between different information and concepts in the provided context, you always provide in-line reference to the user using the index [1], for example [3][7]. You do this so that the user can read the points you considered directly 14 | in the context. Remember you are a team player. In the brainstorming phase, you talk about different approaches that can be taken for the subtask, you think about the challenges you identify and discuss them with the user. Simply come up with approaches and challenges and have a brainstorming, open-ended communication with the user by asking them questions and brainstorming together. Do not reply with structured response in the brainstorming part of the subtask, simply reply in text and paragraphs as if you are having an informal discussion. 15 | After discussion, as you and the user agree on a approach, you follow that approach to come up with the first version of that subtask and again you have a conversation with the user on how you think it can be improved and what feedback the user has and how you came up with this response. Again, please provide in-line reference to the context, it is absolutely essential. 16 | You have back and forth conversations and many drafts of the subtasks are considered until you and the user agree on a draft, once you and the user agree you move on to the next subtask and repeat the entire brainstorming process again for this subtask then the first draft and so and so forth. Within this entire conversation, do not hesitate to ask the user questions about any information that you need and to be on the same page. 17 | keep it chill, creative and engaging man!` 18 | }; 19 | 20 | export async function POST(request: Request) { 21 | try { 22 | const { messages } = await request.json(); 23 | 24 | console.log(messages); 25 | 26 | // Validate the messages format 27 | if (!Array.isArray(messages)) { 28 | return NextResponse.json({ error: 'Invalid messages format' }, { status: 400 }); 29 | } 30 | 31 | // Prepend the system message 32 | const formattedMessages = [SYSTEM_MESSAGE, ...messages]; 33 | 34 | console.log(formattedMessages); 35 | 36 | // Create a completion using the OpenAI API 37 | const completion = await openai.chat.completions.create({ 38 | model: 'gpt-4o', // Use the specified model 39 | temperature: 0, // Adjust temperature as needed 40 | messages: formattedMessages, // Pass the conversation history 41 | }); 42 | 43 | const responseMessage = completion.choices[0].message; 44 | 45 | return NextResponse.json({ message: responseMessage }); 46 | } catch (error) { 47 | console.error('Error in chat API route:', error); 48 | return NextResponse.json({ error: 'Failed to generate response' }, { status: 500 }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/api/followUp/route.ts: -------------------------------------------------------------------------------- 1 | // api/followUp/route.ts 2 | import { NextResponse } from "next/server"; 3 | import OpenAI from "openai"; 4 | 5 | const openai = new OpenAI({ 6 | apiKey: process.env.OPENAI_API_KEY, // Ensure you have your API key in environment variables 7 | }); 8 | 9 | export async function POST(request: Request) { 10 | try { 11 | const { topic, content } = await request.json(); 12 | 13 | // Define the prompt for ChatGPT, including instructions to output JSON 14 | const prompt = ` 15 | You are an expert researcher. The following is the content related to the topic "${topic}": 16 | 17 | "${content}" 18 | 19 | Based on this content, generate 5 follow-up Google search queries that would logically come next in researching this topic. Provide them in the following JSON format without any additional text: 20 | 21 | { 22 | "google_search":[ 23 | "follow_up_one", 24 | "follow_up_two", 25 | "follow_up_three", 26 | "follow_up_four", 27 | "follow_up_five" 28 | ] 29 | } 30 | 31 | Please ensure that the output is valid JSON and does not include any additional explanation or text. 32 | `; 33 | 34 | const completion = await openai.chat.completions.create({ 35 | model: "gpt-4o", 36 | temperature: 0, 37 | messages: [{ role: "user", content: prompt }], 38 | }); 39 | 40 | const responseText = completion.choices[0].message.content; 41 | const cleanResponse = responseText.replace("```json", "").replace("```", "").trim(); 42 | console.log(cleanResponse); 43 | 44 | // Parse the responseText to JSON 45 | const data = JSON.parse(cleanResponse); 46 | 47 | return NextResponse.json(data); 48 | } catch (error) { 49 | console.error("Error in followUp API route:", error); 50 | return NextResponse.json( 51 | { error: "Failed to generate follow-up queries" }, 52 | { status: 500 } 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/api/generateDetails/route.ts: -------------------------------------------------------------------------------- 1 | // app/api/generateDetails/route.ts 2 | import { NextResponse } from "next/server"; 3 | import OpenAI from "openai"; 4 | 5 | const openai = new OpenAI({ 6 | apiKey: process.env.OPENAI_API_KEY, 7 | }); 8 | 9 | export async function POST(request: Request) { 10 | try { 11 | const { prompt } = await request.json(); 12 | 13 | // Call OpenAI API with the prompt 14 | const completion = await openai.chat.completions.create({ 15 | model: "gpt-4o", // Use the appropriate model 16 | temperature: 0, 17 | messages: [{ role: "user", content: prompt }], 18 | }); 19 | 20 | const responseText = completion.choices[0].message.content; 21 | 22 | return NextResponse.json({ content: responseText }); 23 | } catch (error) { 24 | console.error("Error in generateDetails API route:", error); 25 | return NextResponse.json( 26 | { error: "Failed to generate detailed content." }, 27 | { status: 500 } 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/api/generateGraph/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import OpenAI from "openai"; 3 | 4 | const openai = new OpenAI({ 5 | apiKey: process.env.OPENAI_API_KEY, // Ensure you have your API key in environment variables 6 | }); 7 | 8 | export async function POST(request: Request) { 9 | try { 10 | const { query } = await request.json(); 11 | 12 | const areas = `Extensive List of Distinct Areas: 13 | 14 | Historical Perspective 15 | 16 | Explore the origin and evolution of the topic over time. 17 | Technological Impact 18 | 19 | Investigate how technology influences or is influenced by the topic. 20 | Ethical Considerations 21 | 22 | Examine the moral principles and ethical debates surrounding the topic. 23 | Societal Impact 24 | 25 | Analyze how the topic affects society and social dynamics. 26 | Economic Factors 27 | 28 | Assess the economic implications, including costs, benefits, and market effects. 29 | Legal and Regulatory Issues 30 | 31 | Explore laws, regulations, and legal challenges related to the topic. 32 | Environmental Impact 33 | 34 | Investigate the effects on the environment and ecological systems. 35 | Cultural Significance 36 | 37 | Examine the topic's influence on culture, traditions, and cultural expressions. 38 | Psychological Aspects 39 | 40 | Analyze psychological factors and human behavior related to the topic. 41 | Health and Wellness Implications 42 | 43 | Explore the impact on physical and mental health. 44 | Educational Applications 45 | 46 | Investigate how the topic is taught, learned, and applied in educational settings. 47 | Future Trends and Predictions 48 | 49 | Assess potential future developments and forecasts. 50 | Case Studies and Examples 51 | 52 | Analyze specific instances to illustrate practical applications. 53 | Best Practices and Methodologies 54 | 55 | Identify the most effective approaches and methods. 56 | Challenges and Solutions 57 | 58 | Examine the problems faced and potential solutions. 59 | Innovations and Advancements 60 | 61 | Explore recent developments and breakthroughs. 62 | Global Perspectives 63 | 64 | Analyze the topic from international or multicultural viewpoints. 65 | Statistical Analysis and Data 66 | 67 | Use quantitative data to understand trends and patterns. 68 | Philosophical Underpinnings 69 | 70 | Examine fundamental theories and philosophies related to the topic. 71 | Policy Implications 72 | 73 | Assess how the topic influences or is influenced by public policy. 74 | Industry Applications 75 | 76 | Explore how different industries apply or are affected by the topic. 77 | Cross-Disciplinary Connections 78 | 79 | Investigate links between the topic and other fields of study. 80 | User Experience and Human Factors 81 | 82 | Analyze interactions between humans and the topic. 83 | Security and Privacy Concerns 84 | 85 | Explore risks related to security and privacy. 86 | Design Principles 87 | 88 | Examine design aspects, aesthetics, and functionality. 89 | Theoretical Frameworks 90 | 91 | Explore theories and models that explain the topic. 92 | Practical Implementations 93 | 94 | Investigate real-world applications and practices. 95 | Research Methodologies 96 | 97 | Examine methods used to study and analyze the topic. 98 | Stakeholder Analysis 99 | 100 | Identify and assess the interests of different stakeholders. 101 | Success Metrics and Evaluation 102 | 103 | Determine how success is measured and evaluated. 104 | Failure Analysis 105 | 106 | Study failures to understand pitfalls and lessons learned. 107 | Marketing and Communication Strategies 108 | 109 | Explore how the topic is promoted or communicated. 110 | Competitor Analysis 111 | 112 | Assess competitive landscapes related to the topic. 113 | Consumer Behavior 114 | 115 | Analyze how consumers interact with or are affected by the topic. 116 | Resource Management 117 | 118 | Explore how resources are allocated and managed. 119 | Risk Assessment and Management 120 | 121 | Identify risks and strategies to mitigate them. 122 | Quality Assurance and Control 123 | 124 | Examine processes to maintain standards and quality. 125 | Scalability and Growth 126 | 127 | Assess potential for expansion and scalability. 128 | Accessibility and Inclusivity 129 | 130 | Investigate how accessible and inclusive the topic is. 131 | Collaboration and Partnerships 132 | 133 | Explore opportunities for collaboration and alliances. 134 | Change Management 135 | 136 | Examine how changes are implemented and managed. 137 | Cultural Adaptation 138 | 139 | Assess how the topic adapts across different cultures. 140 | Professional Development 141 | 142 | Explore opportunities for learning and career advancement. 143 | Ethical Dilemmas and Controversies 144 | 145 | Examine contentious issues and moral conflicts. 146 | Legal Precedents and Case Law 147 | 148 | Study legal cases that have shaped the topic. 149 | Behavioral Economics 150 | 151 | Analyze economic decisions influenced by human behavior. 152 | Innovation Diffusion 153 | 154 | Explore how innovations spread within markets or societies. 155 | Network Effects 156 | 157 | Assess how the value increases as more people use the topic. 158 | Social Media Influence 159 | 160 | Examine the role of social media in shaping perceptions. 161 | Disruption Potential 162 | 163 | Assess how the topic could disrupt existing systems or industries. 164 | Privacy Issues 165 | 166 | Investigate concerns related to data and personal privacy. 167 | Automation and AI Impact 168 | 169 | Explore effects of automation and artificial intelligence. 170 | Sustainability Practices 171 | 172 | Examine sustainable approaches and environmental stewardship. 173 | Supply Chain and Logistics 174 | 175 | Analyze operational aspects related to supply chains. 176 | Financial Modeling and Investment 177 | 178 | Explore financial aspects and investment opportunities. 179 | Demographic Trends 180 | 181 | Investigate how population dynamics affect the topic. 182 | Urban Planning and Development 183 | 184 | Explore implications for urban environments. 185 | Education and Training Needs 186 | 187 | Assess educational requirements and skill development. 188 | Regulatory Compliance 189 | 190 | Examine adherence to laws and regulations. 191 | Organizational Behavior and Leadership 192 | 193 | Analyze how organizations interact with the topic. 194 | Team Dynamics and Collaboration 195 | 196 | Explore teamwork and collaborative aspects. 197 | Conflict Resolution 198 | 199 | Examine strategies for managing and resolving conflicts. 200 | Decision-Making Processes 201 | 202 | Analyze how decisions are made within the context of the topic. 203 | Cognitive Biases 204 | 205 | Investigate biases that may influence understanding or decisions. 206 | Motivation and Engagement 207 | 208 | Explore factors that drive participation and interest. 209 | Corporate Social Responsibility 210 | 211 | Assess ethical responsibilities and societal contributions. 212 | Brand Identity and Reputation 213 | 214 | Examine impact on branding and public image. 215 | Data Analytics and Big Data 216 | 217 | Explore data-driven insights and analytics. 218 | Cybersecurity Threats 219 | 220 | Investigate security risks and protective measures. 221 | Disaster Recovery and Business Continuity 222 | 223 | Assess preparedness for disruptions and emergencies. 224 | Cloud Computing and Virtualization 225 | 226 | Explore infrastructure and technology solutions. 227 | Internet of Things (IoT) Integration 228 | 229 | Examine connectivity and smart technology applications. 230 | Human-Computer Interaction 231 | 232 | Analyze interfaces between humans and technology. 233 | Augmented and Virtual Reality Applications 234 | 235 | Explore immersive technologies and their uses. 236 | User Interface and Experience Design 237 | 238 | Examine design considerations for usability. 239 | Sustainable Design and Architecture 240 | 241 | Investigate environmentally friendly design practices. 242 | Customer Satisfaction and Feedback 243 | 244 | Assess customer experiences and feedback mechanisms. 245 | Innovation Ecosystems 246 | 247 | Explore environments that foster creativity and innovation. 248 | Intellectual Property Issues 249 | 250 | Examine rights, protections, and legal considerations. 251 | Remote Work Trends 252 | 253 | Analyze implications of remote and flexible work arrangements. 254 | Work-Life Balance 255 | 256 | Explore the topic's impact on personal and professional life. 257 | Organizational Culture 258 | 259 | Examine values, beliefs, and behaviors within organizations. 260 | Leadership Styles 261 | 262 | Analyze different approaches to leadership. 263 | Employee Engagement 264 | 265 | Assess strategies to motivate and involve employees. 266 | Training and Development Programs 267 | 268 | Explore professional growth and skill enhancement opportunities. 269 | Diversity and Inclusion 270 | 271 | Examine efforts to promote equality and diversity. 272 | Emotional Intelligence 273 | 274 | Analyze the role of emotions in interactions related to the topic. 275 | Corporate Governance 276 | 277 | Examine structures and policies for organizational oversight. 278 | Globalization Effects 279 | 280 | Explore international influences and global integration. 281 | Monetary Policy and Economic Theories 282 | 283 | Assess economic principles relevant to the topic. 284 | Environmental Policies and Regulations 285 | 286 | Examine laws aimed at protecting the environment. 287 | Crisis Management 288 | 289 | Investigate strategies for handling crises and emergencies. 290 | Social Movements and Activism 291 | 292 | Explore grassroots efforts and advocacy related to the topic. 293 | Media and Communication Channels 294 | 295 | Analyze how information is disseminated and consumed. 296 | Cognitive Science and Neuroscience 297 | 298 | Examine mental processes and brain functions. 299 | Behavioral Psychology 300 | 301 | Analyze behavior patterns and psychological influences. 302 | Ethical Marketing 303 | 304 | Explore marketing practices that adhere to ethical standards. 305 | Data Privacy Regulations 306 | 307 | Examine legal requirements for protecting personal data. 308 | Future of Work 309 | 310 | Assess emerging trends affecting employment and careers. 311 | Educational Technology - Explore technological tools used in education. 312 | ` 313 | 314 | // Define the prompt for ChatGPT, including instructions to output JSON 315 | const prompt = ` 316 | You are an expert in researching a topic from various angles using google. When presented with a query, 317 | provide a research plan. Please ensure that every google search covers a topic that is different 318 | from other google searches in order to avoid redundant and repetitive information. For any given query, 319 | provide a list of google searches from varying areas and perspectives such that a large breadth of information 320 | is retrieved related to that query in a manner that avoids redundant and repetitive information. Please choose 5 distinct areas. 321 | 322 | Please provide a JSON response in the following format without any additional text: 323 | { 324 | "areas": [ 325 | { 326 | "name": "", 327 | "reason": "", 328 | "google_search_ideas": [ 329 | "", 330 | "", 331 | "" 332 | ] 333 | } 334 | // Remaining 4 areas 335 | ] 336 | } 337 | 338 | As you generate the queries, please keep the following characteristics of a good google query in mind: 339 | 340 | 1. Specific: It includes precise keywords related to the topic. Avoid general or vague terms. Example: Instead of "climate change," search for "effects of climate change on Arctic wildlife." 341 | 2. Concise: It avoids unnecessary words or phrases. Keep the query short and focused. Example: Use "best laptops for programming" instead of "what are the best laptops for programming in 2024?" 342 | 3. Descriptive: It uses words that describe the content you want, such as "tutorial," "definition," "examples," "statistics," or "benefits." Example: "machine learning tutorial for beginners." 343 | 4. Keyword-Rich: Includes primary and secondary keywords relevant to the search. Example: "renewable energy advantages and disadvantages." 344 | 5. Logical Operators (if needed): Uses quotes, plus, minus, or logical operators (AND, OR) to refine results. Example: "data science" AND "job trends" 2024. 345 | 6. Use of Natural Language: When appropriate, phrased as a question or in natural language. Example: "How does photosynthesis work?" 346 | 7. Exclusion of Irrelevant Terms: Uses a minus sign (-) to exclude unwanted topics. Example: "python programming -snake." 347 | 8. Use of Filters (if applicable): Adds time, location, or file type for precision. Example: "climate change report filetype:pdf." 348 | 349 | Please only reply in the given JSON format and with nothing else. 350 | "${query}" 351 | `; 352 | 353 | const completion = await openai.chat.completions.create({ 354 | model: "gpt-4o", 355 | temperature: 0, 356 | messages: [{ role: "user", content: prompt }], 357 | }); 358 | 359 | const responseText = completion.choices[0].message.content; 360 | const cleanResponse = responseText.replace("\`\`\`json", "").replace("\`\`\`", "") 361 | console.log(cleanResponse) 362 | 363 | // Parse the responseText to JSON 364 | const data = JSON.parse(cleanResponse); 365 | 366 | return NextResponse.json(data); 367 | } catch (error) { 368 | console.error("Error in generateGraph API route:", error); 369 | return NextResponse.json( 370 | { error: "Failed to generate graph data" }, 371 | { status: 500 } 372 | ); 373 | } 374 | } 375 | -------------------------------------------------------------------------------- /app/api/generateSearchQueries/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import OpenAI from 'openai'; 3 | 4 | const openai = new OpenAI({ 5 | apiKey: process.env.OPENAI_API_KEY, 6 | }); 7 | 8 | export async function POST(request: Request) { 9 | try { 10 | const { nodeContent, highlightedText } = await request.json(); 11 | 12 | const prompt = ` 13 | You are a researcher. Given the full node content below and a highlighted portion the user is interested in, generate exactly 3 Google search queries that would help find more details about the highlighted part. 14 | 15 | Node Content: 16 | "${nodeContent}" 17 | 18 | Highlighted Text: 19 | "${highlightedText}" 20 | 21 | Output as JSON: 22 | { 23 | "searchQueries": [ 24 | "query_1", 25 | "query_2", 26 | "query_3" 27 | ] 28 | } 29 | `; 30 | 31 | const completion = await openai.chat.completions.create({ 32 | model: 'gpt-4o', // Use the appropriate model 33 | temperature: 0, 34 | messages: [{ role: 'user', content: prompt }], 35 | }); 36 | 37 | const responseText = completion.choices[0].message.content; 38 | const cleanResponse = responseText.trim().replace(/```json|```/g, ''); 39 | const data = JSON.parse(cleanResponse); 40 | 41 | return NextResponse.json(data); 42 | } catch (error) { 43 | console.error('Error in generateSearchQueries API route:', error); 44 | return NextResponse.json( 45 | { error: 'Failed to generate search queries.' }, 46 | { status: 500 } 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/api/generateSpreadsheetColumns/route.ts: -------------------------------------------------------------------------------- 1 | // app/api/generateSpreadsheetColumns/route.ts 2 | import { NextResponse } from "next/server"; 3 | import OpenAI from "openai"; 4 | 5 | const openai = new OpenAI({ 6 | apiKey: process.env.OPENAI_API_KEY, // Ensure you have your API key in environment variables 7 | }); 8 | 9 | export async function POST(request: Request) { 10 | try { 11 | const { purpose } = await request.json(); 12 | 13 | // Define the prompt for ChatGPT 14 | const prompt = ` 15 | You are an expert spreadsheet designer. Based on the user's purpose for a spreadsheet, design the most suitable spreadsheet structure. 16 | 17 | The user's purpose is: 18 | "${purpose}" 19 | 20 | Possible column types include: 21 | - Text 22 | - Number 23 | - Currency 24 | - Date d-m-y 25 | - Date m-d-y 26 | - Checkbox 27 | - Select 28 | - Label 29 | 30 | For each column, specify: 31 | - id: Unique identifier for the column. 32 | - name: The name of the column. 33 | - description: A brief description of what data the column holds. 34 | - type: The column type (from the possible types above). 35 | - options: For Select and Label types, specify the options available. 36 | 37 | Please provide a JSON response in the following format without any additional text: 38 | 39 | { 40 | "columns": [ 41 | { 42 | "id": "", 43 | "name": "", 44 | "description": "", 45 | "type": "", 46 | "options": [ "", "" ] // Include if type is Select or Label 47 | } 48 | // Add more columns as needed 49 | ] 50 | } 51 | `; 52 | 53 | const completion = await openai.chat.completions.create({ 54 | model: "gpt-4o", 55 | temperature: 0, 56 | messages: [{ role: "user", content: prompt }], 57 | }); 58 | 59 | const responseText = completion.choices[0].message.content; 60 | const cleanResponse = responseText.replace("```json", "").replace("```", ""); 61 | console.log(cleanResponse); 62 | 63 | // Parse the responseText to JSON 64 | const data = JSON.parse(cleanResponse); 65 | 66 | // Ensure each column has a unique ID 67 | data.columns = data.columns.map((col: any) => ({ 68 | ...col, 69 | id: col.id || require("crypto").randomBytes(16).toString("hex"), 70 | })); 71 | 72 | return NextResponse.json(data); 73 | } catch (error) { 74 | console.error("Error in generateSpreadsheetColumns API route:", error); 75 | return NextResponse.json( 76 | { error: "Failed to generate column definitions" }, 77 | { status: 500 } 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /app/api/researchDetails/route.ts: -------------------------------------------------------------------------------- 1 | // app/api/researchDetails/route.ts 2 | import { NextResponse } from 'next/server'; 3 | import OpenAI from 'openai'; 4 | import { tavily } from '@tavily/core'; 5 | 6 | const openai = new OpenAI({ 7 | apiKey: process.env.OPENAI_API_KEY, 8 | }); 9 | 10 | export async function POST(request: Request) { 11 | try { 12 | const { query } = await request.json(); 13 | const tvly = tavily({ apiKey: process.env.TAVILY_API_KEY }); 14 | 15 | // Fetch search results 16 | const response = await tvly.search(query, { 17 | searchDepth: 'advanced', 18 | includeRawContent: true, 19 | maxResults: 3, 20 | }); 21 | 22 | // Filter out results where rawContent is null 23 | const validResults = response.results.filter( 24 | (result) => result.rawContent && result.rawContent.trim() !== '' 25 | ); 26 | 27 | // Store raw content, title, summary, and URL for each result 28 | const allSources = {}; 29 | validResults.forEach((result) => { 30 | const { url, title, content, rawContent } = result; 31 | allSources[url] = { 32 | title, 33 | summary: content, 34 | rawContent, 35 | }; 36 | }); 37 | 38 | // Prepare data for the LLM 39 | const formattedResults = validResults 40 | .map((result) => { 41 | return `Title: ${result.title}\nURL: ${result.url}\nSummary: ${result.content}\nRaw Content: ${result.rawContent}\n`; 42 | }) 43 | .join('\n---\n'); 44 | 45 | // Construct the prompt 46 | const prompt = ` 47 | You are a skilled writer. Using the following search results, extract every detail (even the trivial ones) related to: "${query}". Include all relevant information. Do not miss any relevant details. 48 | 49 | Write your findings in 4-5 detailed bullet points. 50 | 51 | Please mention all sources in-line using markdown format for each bullet point. For the source, use the source name as display. 52 | 53 | Search results: 54 | ${formattedResults} 55 | 56 | Provide the output in Markdown format. 57 | `; 58 | 59 | // Call OpenAI API with the prompt 60 | const completion = await openai.chat.completions.create({ 61 | model: 'gpt-4o', // Use the appropriate model 62 | temperature: 0, 63 | messages: [{ role: 'user', content: prompt }], 64 | }); 65 | 66 | const responseText = completion.choices[0].message.content; 67 | 68 | return NextResponse.json({ 69 | content: responseText.trim(), 70 | sources: allSources, 71 | }); 72 | } catch (error) { 73 | console.error('Error in researchDetails API route:', error); 74 | return NextResponse.json( 75 | { error: 'Failed to fetch data from OpenAI.' }, 76 | { status: 500 } 77 | ); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/api/researchIdea/route.ts: -------------------------------------------------------------------------------- 1 | // app/api/researchIdea/route.ts 2 | import { NextResponse } from "next/server"; 3 | 4 | const perplexityToken = process.env.PERPLEXITY_TOKEN || ""; 5 | 6 | const MAX_TRIES = 3; // Retain if needed, though we do a single Perplexity call here 7 | 8 | export async function POST(request: Request) { 9 | try { 10 | // We still retrieve ideaNode (and rootNodeId if needed) from the request JSON 11 | const { ideaNode, rootNodeId } = await request.json(); // { nodeId, searchQuery } 12 | console.log(ideaNode); 13 | 14 | const { nodeId, searchQuery } = ideaNode; 15 | 16 | // --- Replace all the Tavily/Firecrawl/OpenAI logic with a single Perplexity request. --- 17 | // If you need multiple attempts, you can wrap this in a retry loop or handle errors accordingly. 18 | 19 | // Build the request body for Perplexity 20 | const perplexityBody = { 21 | model: "sonar-pro", 22 | messages: [ 23 | { 24 | role: "system", 25 | content: 26 | "Be precise and detailed. Write your answer in 3-4 detailed paragraphs. No subheadings. No bullet points. Highlight important parts by making the text bold. Always provide in-text citations in markdown e.g. [NYT](https://nytimes.com)", 27 | }, 28 | { role: "user", content: searchQuery }, 29 | ], 30 | temperature: 0.2, 31 | top_p: 0.9, 32 | search_domain_filter: ["perplexity.ai"], 33 | return_images: false, 34 | return_related_questions: false, 35 | search_recency_filter: "month", 36 | top_k: 0, 37 | stream: false, 38 | presence_penalty: 0, 39 | frequency_penalty: 1, 40 | response_format: null, 41 | }; 42 | 43 | // Make the Perplexity API call 44 | const res = await fetch("https://api.perplexity.ai/chat/completions", { 45 | method: "POST", 46 | headers: { 47 | Authorization: `Bearer ${perplexityToken}`, 48 | "Content-Type": "application/json", 49 | }, 50 | body: JSON.stringify(perplexityBody), 51 | }); 52 | 53 | if (!res.ok) { 54 | console.error(`Failed to fetch from Perplexity. Status: ${res.status}`); 55 | return NextResponse.json( 56 | { error: "Failed to fetch from Perplexity." }, 57 | { status: 500 } 58 | ); 59 | } 60 | 61 | const data = await res.json(); 62 | console.log("Fetched Perplexity response:", data); 63 | 64 | // The main text answer from Perplexity 65 | const responseTextFinal = 66 | data?.choices?.[0]?.message?.content?.trim() || "Information Not Found"; 67 | 68 | // Build your sources object from the Perplexity `citations` array 69 | const allSources: { [key: string]: any } = {}; 70 | if (data?.citations && Array.isArray(data.citations)) { 71 | data.citations.forEach((citationUrl: string) => { 72 | allSources[citationUrl] = { 73 | title: "", 74 | summary: "", 75 | rawContent: "", 76 | }; 77 | }); 78 | } 79 | 80 | // --- Return the response in the same final structure as before --- 81 | return NextResponse.json({ 82 | response: { 83 | nodeId, 84 | bulletPoints: responseTextFinal, // The Perplexity answer as bulletPoints equivalent 85 | sources: allSources, // The new source mapping from Perplexity citations 86 | }, 87 | }); 88 | } catch (error) { 89 | console.error("Error in researchIdea API route:", error); 90 | return NextResponse.json( 91 | { error: "Failed to fetch data from Perplexity." }, 92 | { status: 500 } 93 | ); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /app/auth/signin/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { signIn } from "next-auth/react"; 4 | import { Button } from "@/components/ui/button"; 5 | import { FcGoogle } from "react-icons/fc"; 6 | import { motion } from "framer-motion"; 7 | 8 | export default function SignInForm() { 9 | return ( 10 | 16 |
17 | 23 | Sign in to your account 24 | 25 |

26 | Use your Google account to continue 27 |

28 |
29 | 33 | 40 | 41 |
42 | ); 43 | } 44 | 45 | -------------------------------------------------------------------------------- /app/demo/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import React from "react" 4 | import { Bar } from "react-chartjs-2" 5 | import { 6 | Chart as ChartJS, 7 | BarElement, 8 | CategoryScale, 9 | LinearScale, 10 | Tooltip, 11 | Legend, 12 | } from "chart.js" 13 | 14 | // Register Chart.js components 15 | ChartJS.register(CategoryScale, LinearScale, BarElement, Tooltip, Legend) 16 | 17 | export default function ComplexGanttChart() { 18 | // Task data 19 | const tasks = [ 20 | { name: "1.1 Wireframes (Joe)", start: 0, duration: 3 }, 21 | { name: "1.2 Mockups (Joe)", start: 3, duration: 5 }, 22 | { name: "1.3 Finalize Design (Joe)", start: 8, duration: 2 }, 23 | { name: "2.1 Setup Project (Mark)", start: 10, duration: 2 }, 24 | { name: "2.2 Responsive Layout (Mark)", start: 12, duration: 5 }, 25 | { name: "2.3 Integrate Design (Mark)", start: 17, duration: 3 }, 26 | { name: "3.1 Setup Server (John)", start: 10, duration: 4 }, 27 | { name: "3.2 Build API (John)", start: 14, duration: 6 }, 28 | { name: "3.3 Integrate Frontend (John)", start: 20, duration: 3 }, 29 | { name: "4.1 Testing (Mark & John)", start: 23, duration: 3 }, 30 | { name: "4.2 Deployment (Mark & John)", start: 26, duration: 2 }, 31 | ] 32 | 33 | // Chart datasets 34 | const data = { 35 | labels: tasks.map((task) => task.name), // Task names 36 | datasets: [ 37 | { 38 | label: "Start", 39 | data: tasks.map((task) => task.start), 40 | backgroundColor: "rgba(0, 0, 0, 0)", // Invisible offset 41 | }, 42 | { 43 | label: "Duration", 44 | data: tasks.map((task) => task.duration), 45 | backgroundColor: tasks.map((task) => { 46 | // Assign colors based on the person responsible 47 | if (task.name.includes("(Joe)")) return "rgba(75, 192, 192, 0.7)" // Joe: Green 48 | if (task.name.includes("(Mark)")) return "rgba(153, 102, 255, 0.7)" // Mark: Purple 49 | if (task.name.includes("(John)")) return "rgba(255, 159, 64, 0.7)" // John: Orange 50 | return "rgba(201, 203, 207, 0.7)" // Default: Grey 51 | }), 52 | }, 53 | ], 54 | } 55 | 56 | const options = { 57 | responsive: true, 58 | maintainAspectRatio: false, 59 | plugins: { 60 | legend: { 61 | display: false, // Hides legend for simplicity 62 | }, 63 | tooltip: { 64 | callbacks: { 65 | label: (tooltipItem) => { 66 | const datasetLabel = tooltipItem.dataset.label 67 | const value = tooltipItem.raw 68 | return `${datasetLabel}: ${value} days` 69 | }, 70 | }, 71 | }, 72 | }, 73 | indexAxis: "y", // Horizontal bars 74 | scales: { 75 | x: { 76 | stacked: true, 77 | title: { 78 | display: true, 79 | text: "Time (Days)", 80 | }, 81 | }, 82 | y: { 83 | stacked: true, 84 | title: { 85 | display: false, 86 | }, 87 | barThickness: 3, // Set a fixed thickness for the bars 88 | }, 89 | }, 90 | } 91 | 92 | 93 | return ( 94 |
95 |

Website Development Gantt Chart

96 |
97 | 98 |
99 |
100 | 101 | 102 |
103 |
104 | ) 105 | } 106 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoibloya/nuggt-research/c67c86cf2338414da0530e25e4dd797cf3455a6e/app/favicon.ico -------------------------------------------------------------------------------- /app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoibloya/nuggt-research/c67c86cf2338414da0530e25e4dd797cf3455a6e/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoibloya/nuggt-research/c67c86cf2338414da0530e25e4dd797cf3455a6e/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 base { 10 | :root { 11 | --background: 0 0% 100%; 12 | --foreground: 0 0% 3.9%; 13 | --card: 0 0% 100%; 14 | --card-foreground: 0 0% 3.9%; 15 | --popover: 0 0% 100%; 16 | --popover-foreground: 0 0% 3.9%; 17 | --primary: 0 0% 9%; 18 | --primary-foreground: 0 0% 98%; 19 | --secondary: 0 0% 96.1%; 20 | --secondary-foreground: 0 0% 9%; 21 | --muted: 0 0% 96.1%; 22 | --muted-foreground: 0 0% 45.1%; 23 | --accent: 0 0% 96.1%; 24 | --accent-foreground: 0 0% 9%; 25 | --destructive: 0 84.2% 60.2%; 26 | --destructive-foreground: 0 0% 98%; 27 | --border: 0 0% 89.8%; 28 | --input: 0 0% 89.8%; 29 | --ring: 0 0% 3.9%; 30 | --chart-1: 12 76% 61%; 31 | --chart-2: 173 58% 39%; 32 | --chart-3: 197 37% 24%; 33 | --chart-4: 43 74% 66%; 34 | --chart-5: 27 87% 67%; 35 | --radius: 0.5rem; 36 | } 37 | .dark { 38 | --background: 0 0% 3.9%; 39 | --foreground: 0 0% 98%; 40 | --card: 0 0% 3.9%; 41 | --card-foreground: 0 0% 98%; 42 | --popover: 0 0% 3.9%; 43 | --popover-foreground: 0 0% 98%; 44 | --primary: 0 0% 98%; 45 | --primary-foreground: 0 0% 9%; 46 | --secondary: 0 0% 14.9%; 47 | --secondary-foreground: 0 0% 98%; 48 | --muted: 0 0% 14.9%; 49 | --muted-foreground: 0 0% 63.9%; 50 | --accent: 0 0% 14.9%; 51 | --accent-foreground: 0 0% 98%; 52 | --destructive: 0 62.8% 30.6%; 53 | --destructive-foreground: 0 0% 98%; 54 | --border: 0 0% 14.9%; 55 | --input: 0 0% 14.9%; 56 | --ring: 0 0% 83.1%; 57 | --chart-1: 220 70% 50%; 58 | --chart-2: 160 60% 45%; 59 | --chart-3: 30 80% 55%; 60 | --chart-4: 280 65% 60%; 61 | --chart-5: 340 75% 55%; 62 | } 63 | } 64 | 65 | @layer base { 66 | * { 67 | @apply border-border; 68 | } 69 | body { 70 | @apply bg-background text-foreground; 71 | } 72 | } 73 | 74 | 75 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import localFont from "next/font/local"; 3 | import "./globals.css"; 4 | import { CSPostHogProvider } from './providers' 5 | 6 | 7 | const geistSans = localFont({ 8 | src: "./fonts/GeistVF.woff", 9 | variable: "--font-geist-sans", 10 | weight: "100 900", 11 | }); 12 | const geistMono = localFont({ 13 | src: "./fonts/GeistMonoVF.woff", 14 | variable: "--font-geist-mono", 15 | weight: "100 900", 16 | }); 17 | 18 | export const metadata: Metadata = { 19 | title: "Create Next App", 20 | description: "Generated by create next app", 21 | }; 22 | 23 | export default function RootLayout({ 24 | children, 25 | }: Readonly<{ 26 | children: React.ReactNode; 27 | }>) { 28 | return ( 29 | 30 | 31 | 34 | 35 | {children} 36 | 37 | 38 | 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import FlowCanvas from "@/components/FlowCanvas"; 5 | import SearchBox from "@/components/SearchBox"; 6 | import AreaSelection from "@/components/AreaSelection"; 7 | import { ButtonLoading } from "@/components/ButtonLoading"; 8 | import { motion, AnimatePresence } from "framer-motion"; 9 | import dagre from "dagre"; 10 | import { DockDemo } from "@/components/DockDemo"; 11 | import { v4 as uuidv4 } from "uuid"; 12 | import { useFlowStore } from "@/storage/store"; 13 | import SpreadsheetDialog from "@/components/SpreadsheetDialog"; 14 | import ModernChatbot from "@/components/ModernChatbot"; 15 | import ContextDialog from "@/components/ContextDialog"; 16 | import { Card } from "@/components/ui/card"; 17 | import { Button } from "@/components/ui/button"; 18 | import { cn } from "@/lib/utils"; 19 | import { DotPattern } from "@/components/ui/dot-pattern"; 20 | 21 | import { doc, getDoc, setDoc, updateDoc } from "firebase/firestore"; 22 | import { db, auth } from "@/lib/firebase"; 23 | import { 24 | onAuthStateChanged, 25 | signInWithPopup, 26 | GoogleAuthProvider, 27 | signOut, 28 | } from "firebase/auth"; 29 | import { cleanData } from "@/utils/cleanData"; 30 | 31 | const nodeWidth = 200; 32 | const nodeHeight = 50; 33 | 34 | const FlowPage = () => { 35 | const { 36 | showFlow, 37 | setShowFlow, 38 | query, 39 | setQuery, 40 | nodes, 41 | setNodes, 42 | edges, 43 | setEdges, 44 | loading, 45 | setLoading, 46 | ideaNodesArray, 47 | setIdeaNodesArray, 48 | areasData, 49 | setAreasData, 50 | } = useFlowStore(); 51 | 52 | const [showSearchBox, setShowSearchBox] = React.useState(true); 53 | const [showAreaSelection, setShowAreaSelection] = React.useState(false); 54 | 55 | const [user, setUser] = React.useState(null); 56 | const [authLoading, setAuthLoading] = React.useState(true); 57 | 58 | const [dataLoaded, setDataLoaded] = React.useState(false); 59 | const [docExists, setDocExists] = React.useState(false); 60 | 61 | // Track if we have unsaved changes 62 | const changesPendingRef = React.useRef(false); 63 | 64 | // Track saving state for button 65 | const [isSaving, setIsSaving] = React.useState(false); 66 | 67 | // Listen for auth state changes 68 | React.useEffect(() => { 69 | const unsubscribe = onAuthStateChanged(auth, (currentUser) => { 70 | setUser(currentUser); 71 | setAuthLoading(false); 72 | }); 73 | 74 | return () => unsubscribe(); 75 | }, []); 76 | 77 | // Sign in with Google 78 | const handleGoogleSignIn = async () => { 79 | const provider = new GoogleAuthProvider(); 80 | try { 81 | await signInWithPopup(auth, provider); 82 | } catch (error) { 83 | console.error("Error signing in with Google:", error); 84 | } 85 | }; 86 | 87 | // Sign out 88 | const handleSignOut = async () => { 89 | try { 90 | await signOut(auth); 91 | } catch (error) { 92 | console.error("Error signing out:", error); 93 | } 94 | }; 95 | 96 | // Generate unique label for newly created nodes 97 | const generateUniqueLabel = (type: string, baseLabel: string): string => { 98 | const count = nodes.filter((node) => node.type === type).length + 1; 99 | return `Untitled-${baseLabel}-${count}`; 100 | }; 101 | 102 | // Handlers for adding new nodes 103 | const addSpreadsheetNode = () => { 104 | const newNodeId = `spreadsheet-${uuidv4()}`; 105 | const uniqueLabel = generateUniqueLabel("spreadsheet", "Spreadsheet"); 106 | const newNode = { 107 | id: newNodeId, 108 | type: "spreadsheet", 109 | position: { 110 | x: 0, 111 | y: 0, 112 | }, 113 | data: { 114 | label: uniqueLabel, 115 | }, 116 | sourcePosition: "right", 117 | targetPosition: "left", 118 | }; 119 | 120 | useFlowStore.getState().addNode(newNode); 121 | immediateSave(); 122 | }; 123 | 124 | const addChatbotNode = () => { 125 | const newNodeId = `chatbot-${uuidv4()}`; 126 | const uniqueLabel = generateUniqueLabel("chatbot", "Chatbot"); 127 | const newNode = { 128 | id: newNodeId, 129 | type: "chatbot", 130 | position: { 131 | x: Math.random() * 600 - 300, 132 | y: Math.random() * 600 - 300, 133 | }, 134 | data: { 135 | label: uniqueLabel, 136 | }, 137 | sourcePosition: "right", 138 | targetPosition: "left", 139 | }; 140 | 141 | useFlowStore.getState().addNode(newNode); 142 | immediateSave(); 143 | }; 144 | 145 | const addContextNode = () => { 146 | const newNodeId = `context-${uuidv4()}`; 147 | const uniqueLabel = generateUniqueLabel("contextNode", "Context"); 148 | const newNode = { 149 | id: newNodeId, 150 | type: "contextNode", 151 | position: { 152 | x: Math.random() * 600 - 300, 153 | y: Math.random() * 600 - 300, 154 | }, 155 | data: { label: uniqueLabel }, 156 | sourcePosition: "right", 157 | targetPosition: "left", 158 | }; 159 | useFlowStore.getState().addNode(newNode); 160 | immediateSave(); 161 | }; 162 | 163 | // Handle search 164 | const handleSubmit = async (event: React.FormEvent) => { 165 | event.preventDefault(); 166 | if (query.trim()) { 167 | setLoading(true); 168 | try { 169 | const response = await fetch("/api/generateGraph", { 170 | method: "POST", 171 | headers: { 172 | "Content-Type": "application/json", 173 | }, 174 | body: JSON.stringify({ query }), 175 | }); 176 | 177 | if (!response.ok) { 178 | throw new Error("Failed to generate graph data"); 179 | } 180 | 181 | const data = await response.json(); 182 | setAreasData(data.areas); 183 | setShowSearchBox(false); 184 | setShowAreaSelection(true); 185 | } catch (error) { 186 | console.error(error); 187 | } finally { 188 | setLoading(false); 189 | } 190 | } 191 | }; 192 | 193 | // Process the selected areas in the AreaSelection component 194 | const handleProcessSelection = (selectedAreasData: any) => { 195 | const { 196 | nodes: generatedNodes, 197 | edges: generatedEdges, 198 | ideaNodesArray: generatedIdeaNodesArray, 199 | } = processGraphData(selectedAreasData); 200 | 201 | setNodes([...nodes, ...generatedNodes]); 202 | setEdges([...edges, ...generatedEdges]); 203 | setIdeaNodesArray([...ideaNodesArray, ...generatedIdeaNodesArray]); 204 | setShowAreaSelection(false); 205 | setAreasData(null); 206 | immediateSave(); 207 | }; 208 | 209 | // Convert data into new nodes and edges 210 | const processGraphData = (areas: any[]) => { 211 | const nodesLocal: any[] = []; 212 | const edgesLocal: any[] = []; 213 | const ideaNodesArrayLocal: any[] = []; 214 | 215 | const rootNodeId = `root-node-${uuidv4()}`; 216 | nodesLocal.push({ 217 | id: rootNodeId, 218 | data: { 219 | label: query, 220 | isRoot: true, 221 | content: "", 222 | }, 223 | type: "expandable", 224 | style: { 225 | backgroundColor: "#ccffcc", 226 | }, 227 | sourcePosition: "right", 228 | targetPosition: "left", 229 | }); 230 | 231 | areas.forEach((area, areaIndex) => { 232 | const areaId = `area-${areaIndex}-${uuidv4()}`; 233 | nodesLocal.push({ 234 | id: areaId, 235 | data: { 236 | label: area.name, 237 | isRoot: false, 238 | content: area.purpose, 239 | }, 240 | type: "expandable", 241 | style: { 242 | backgroundColor: "#ffffff", 243 | }, 244 | sourcePosition: "right", 245 | targetPosition: "left", 246 | }); 247 | 248 | edgesLocal.push({ 249 | id: `e-${rootNodeId}-${areaId}`, 250 | source: rootNodeId, 251 | target: areaId, 252 | type: "smoothstep", 253 | animated: true, 254 | style: { stroke: "#000", strokeWidth: 2 }, 255 | }); 256 | 257 | area.google_search_ideas.forEach((idea: any, ideaIndex: number) => { 258 | const ideaId = `idea-${ideaIndex}-${uuidv4()}`; 259 | nodesLocal.push({ 260 | id: ideaId, 261 | data: { 262 | label: idea.text || idea, 263 | displayLabel: `Waiting to research on ${idea.text || idea}`, 264 | content: "No content available.", 265 | status: "waiting", 266 | }, 267 | type: "expandable", 268 | style: { 269 | backgroundColor: "#f0f0f0", 270 | }, 271 | sourcePosition: "right", 272 | targetPosition: "left", 273 | }); 274 | 275 | ideaNodesArrayLocal.push({ 276 | nodeId: ideaId, 277 | searchQuery: idea.text || idea, 278 | rootNodeId: areaId, 279 | }); 280 | 281 | edgesLocal.push({ 282 | id: `e-${areaId}-${ideaId}`, 283 | source: areaId, 284 | target: ideaId, 285 | type: "smoothstep", 286 | animated: true, 287 | style: { stroke: "#000", strokeWidth: 2 }, 288 | }); 289 | }); 290 | }); 291 | 292 | const { nodes: layoutNodes, edges: layoutEdges } = getLayoutedElements( 293 | nodesLocal, 294 | edgesLocal, 295 | "LR" 296 | ); 297 | 298 | return { 299 | nodes: layoutNodes, 300 | edges: layoutEdges, 301 | ideaNodesArray: ideaNodesArrayLocal, 302 | }; 303 | }; 304 | 305 | // Layout 306 | const getLayoutedElements = (nodes: any[], edges: any[], direction = "LR") => { 307 | const dagreGraph = new dagre.graphlib.Graph(); 308 | dagreGraph.setDefaultEdgeLabel(() => ({})); 309 | 310 | dagreGraph.setGraph({ 311 | rankdir: direction, 312 | nodesep: 100, 313 | ranksep: 200, 314 | marginx: 50, 315 | marginy: 50, 316 | }); 317 | 318 | nodes.forEach((node) => { 319 | dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight }); 320 | }); 321 | 322 | edges.forEach((edge) => { 323 | dagreGraph.setEdge(edge.source, edge.target); 324 | }); 325 | 326 | dagre.layout(dagreGraph); 327 | 328 | nodes.forEach((node) => { 329 | const nodeWithPosition = dagreGraph.node(node.id); 330 | node.position = { 331 | x: nodeWithPosition.x - nodeWidth / 2, 332 | y: nodeWithPosition.y - nodeHeight / 2, 333 | }; 334 | }); 335 | 336 | return { nodes, edges }; 337 | }; 338 | 339 | // Toggle the search box 340 | const onWhatsAppClick = () => { 341 | setShowSearchBox(true); 342 | setShowAreaSelection(false); 343 | }; 344 | 345 | // Load user data if logged in 346 | React.useEffect(() => { 347 | const loadUserData = async () => { 348 | if (user?.email) { 349 | const docRef = doc(db, "users", user.email); 350 | const docSnap = await getDoc(docRef); 351 | 352 | if (docSnap.exists()) { 353 | const data = docSnap.data(); 354 | setDocExists(true); 355 | if (data.nodes) setNodes(data.nodes); 356 | if (data.edges) setEdges(data.edges); 357 | if (data.ideaNodesArray) setIdeaNodesArray(data.ideaNodesArray); 358 | setDataLoaded(true); 359 | } else { 360 | setDocExists(false); 361 | setDataLoaded(true); 362 | } 363 | } 364 | }; 365 | 366 | if (user?.email) { 367 | loadUserData(); 368 | } else { 369 | // If user is not logged in, set dataLoaded to true so the rest can render 370 | setDataLoaded(true); 371 | } 372 | }, [user, setNodes, setEdges, setIdeaNodesArray]); 373 | 374 | // Warn user on refresh/close if there are unsaved changes 375 | React.useEffect(() => { 376 | const handleBeforeUnload = (event: BeforeUnloadEvent) => { 377 | if (changesPendingRef.current) { 378 | event.preventDefault(); 379 | event.returnValue = 380 | "You have unsaved changes. Are you sure you want to leave?"; 381 | } 382 | }; 383 | 384 | window.addEventListener("beforeunload", handleBeforeUnload); 385 | return () => window.removeEventListener("beforeunload", handleBeforeUnload); 386 | }, []); 387 | 388 | // Save data to Firestore (only if user is logged in) 389 | const saveData = async () => { 390 | if (!user?.email || !dataLoaded) return; 391 | 392 | setIsSaving(true); 393 | const docRef = doc(db, "users", user.email); 394 | 395 | const newNodes = cleanData(nodes); 396 | const newEdges = cleanData(edges); 397 | const newIdeaNodesArray = cleanData(ideaNodesArray); 398 | 399 | const updates = { 400 | nodes: newNodes, 401 | edges: newEdges, 402 | ideaNodesArray: newIdeaNodesArray, 403 | lastUpdated: Date.now(), 404 | }; 405 | 406 | try { 407 | if (!docExists) { 408 | await setDoc(docRef, updates, { merge: true }); 409 | setDocExists(true); 410 | } else { 411 | await updateDoc(docRef, updates); 412 | } 413 | console.log("Data saved successfully!"); 414 | changesPendingRef.current = false; // Mark changes as saved 415 | } catch (error) { 416 | console.error("Error saving data:", error); 417 | } finally { 418 | setIsSaving(false); 419 | } 420 | }; 421 | 422 | // Mark changes as pending save 423 | const immediateSave = async () => { 424 | changesPendingRef.current = true; 425 | }; 426 | 427 | // Show a loading indicator if auth is still checking 428 | if (authLoading) { 429 | return ( 430 |
431 | 432 |
433 | ); 434 | } 435 | 436 | // Render the main app 437 | return ( 438 |
446 | {/* Top-right: Login / Logout / Save */} 447 |
457 | {user ? ( 458 | <> 459 | {/* Show Save button only for logged-in users */} 460 | 489 | 490 | {/* Sign Out Button */} 491 | 494 | 495 | ) : ( 496 | // Login button if user is not logged in 497 | 500 | )} 501 |
502 | 503 | {/* Dock with Add Spreadsheet/Chatbot/Context, only if showFlow */} 504 | {showFlow && ( 505 |
514 | 520 |
521 | )} 522 | 523 | {/* Loading overlay */} 524 | {loading && ( 525 |
537 | 538 |
539 | )} 540 | 541 | {/* Center area: SearchBox and AreaSelection */} 542 |
553 | 554 | {showSearchBox && !loading && ( 555 | 562 | 563 | setShowSearchBox(false)} 570 | /> 571 | 572 | 573 | )} 574 | 575 | 576 | 577 | {showAreaSelection && areasData && !loading && ( 578 | 585 | 586 | 590 | 591 | 592 | )} 593 | 594 |
595 | 596 | {/* The main flow / dialogs */} 597 | {showFlow && ( 598 | <> 599 | 605 | 606 | 607 | 608 | 609 | )} 610 |
611 | ); 612 | }; 613 | 614 | export default FlowPage; 615 | -------------------------------------------------------------------------------- /app/providers.tsx: -------------------------------------------------------------------------------- 1 | // app/providers.js 2 | 'use client' 3 | import posthog from 'posthog-js' 4 | import { PostHogProvider } from 'posthog-js/react' 5 | 6 | if (typeof window !== 'undefined') { 7 | posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, { 8 | api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST, 9 | person_profiles: 'identified_only', // or 'always' to create profiles for anonymous users as well 10 | }) 11 | } 12 | export function CSPostHogProvider({ children }) { 13 | return {children} 14 | } -------------------------------------------------------------------------------- /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/AreaSelection.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useState } from "react"; 4 | import { motion } from "framer-motion"; 5 | import { 6 | Card, 7 | CardHeader, 8 | CardTitle, 9 | CardDescription, 10 | CardContent, 11 | CardFooter 12 | } from "@/components/ui/card"; 13 | import { Button } from "@/components/ui/button"; 14 | import { Input } from "@/components/ui/input"; 15 | import { Separator } from "@/components/ui/separator"; 16 | import { PlusCircle, Trash, Edit } from "lucide-react"; 17 | import { Label } from "@/components/ui/label"; 18 | import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; 19 | 20 | interface AreaSelectionProps { 21 | areasData: any[]; 22 | onProcessSelection: (selectedAreasData: any[]) => void; 23 | } 24 | 25 | const AreaSelection: React.FC = ({ areasData, onProcessSelection }) => { 26 | const [areas, setAreas] = useState( 27 | areasData.map((area) => ({ 28 | ...area, 29 | google_search_ideas: area.google_search_ideas.map((idea) => ({ 30 | text: idea, 31 | })), 32 | newIdeaText: "", 33 | })) 34 | ); 35 | 36 | const [newAreaName, setNewAreaName] = useState(""); 37 | 38 | const handleAddArea = () => { 39 | if (newAreaName.trim() === "") return; 40 | setAreas([ 41 | ...areas, 42 | { 43 | name: newAreaName, 44 | purpose: "", 45 | google_search_ideas: [], 46 | newIdeaText: "", 47 | }, 48 | ]); 49 | setNewAreaName(""); 50 | }; 51 | 52 | const handleAddIdea = (areaIndex, ideaText) => { 53 | if (ideaText.trim() === "") return; 54 | const newAreas = [...areas]; 55 | newAreas[areaIndex].google_search_ideas.push({ 56 | text: ideaText, 57 | }); 58 | newAreas[areaIndex].newIdeaText = ""; 59 | setAreas(newAreas); 60 | }; 61 | 62 | const handleDeleteArea = (areaIndex) => { 63 | const newAreas = [...areas]; 64 | newAreas.splice(areaIndex, 1); 65 | setAreas(newAreas); 66 | }; 67 | 68 | const handleDeleteIdea = (areaIndex, ideaIndex) => { 69 | const newAreas = [...areas]; 70 | newAreas[areaIndex].google_search_ideas.splice(ideaIndex, 1); 71 | setAreas(newAreas); 72 | }; 73 | 74 | const handleEditArea = (areaIndex, newName) => { 75 | const newAreas = [...areas]; 76 | newAreas[areaIndex].name = newName; 77 | setAreas(newAreas); 78 | }; 79 | 80 | const handleEditIdea = (areaIndex, ideaIndex, newText) => { 81 | const newAreas = [...areas]; 82 | newAreas[areaIndex].google_search_ideas[ideaIndex].text = newText; 83 | setAreas(newAreas); 84 | }; 85 | 86 | const handleProcess = () => { 87 | const selectedAreas = areas.map((area) => ({ 88 | name: area.name, 89 | purpose: area.purpose, 90 | google_search_ideas: area.google_search_ideas.map((idea) => idea.text), 91 | })); 92 | onProcessSelection(selectedAreas); 93 | }; 94 | 95 | // Just return the card content normally. 96 | // The parent wraps this component in a card, so we don't need another card here. 97 | return ( 98 |
99 |
100 |

101 | Delete Areas You Don't Like Then Click On Proceed 102 |

103 | 106 |
107 | 108 | 109 |
110 | {areas.map((area, areaIndex) => ( 111 | 117 | 118 | 119 |
120 | {area.name} 121 |
122 | 134 | 141 |
142 |
143 |
144 | { 148 | const newAreas = [...areas]; 149 | newAreas[areaIndex].newIdeaText = e.target.value; 150 | setAreas(newAreas); 151 | }} 152 | className="flex-1" 153 | /> 154 | 162 |
163 |
164 | 165 | 166 | 167 | {area.google_search_ideas.map((idea, ideaIndex) => ( 168 |
172 | {idea.text} 173 |
174 | 186 | 195 |
196 |
197 | ))} 198 |
199 |
200 |
201 |
202 | ))} 203 | {/* Add New Area Card */} 204 | 205 | 206 | Add New Area 207 | 208 | Create a new area to explore additional queries. 209 | 210 | 211 | 212 | 213 |
214 | 215 | setNewAreaName(e.target.value)} 220 | /> 221 |
222 |
223 | 224 | 225 | 226 |
227 |
228 | 229 |
230 |
231 | ); 232 | }; 233 | 234 | export default AreaSelection; 235 | -------------------------------------------------------------------------------- /components/ButtonLoading.tsx: -------------------------------------------------------------------------------- 1 | // components/ButtonLoading.tsx 2 | import { Loader2 } from "lucide-react"; 3 | import { Button } from "@/components/ui/button"; 4 | 5 | export function ButtonLoading() { 6 | return ( 7 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /components/ChatbotNode.tsx: -------------------------------------------------------------------------------- 1 | // components/ChatbotNode.tsx 2 | "use client"; 3 | 4 | import React from "react"; 5 | import { 6 | Handle, 7 | NodeProps, 8 | NodeToolbar, 9 | useReactFlow, 10 | Position, 11 | } from "reactflow"; 12 | import { Button } from "@/components/ui/button"; 13 | import { useFlowStore } from "@/storage/store"; 14 | 15 | const ChatbotNode: React.FC = ({ 16 | id, 17 | data, 18 | selected, 19 | style, 20 | sourcePosition = Position.Right, 21 | targetPosition = Position.Left, 22 | }) => { 23 | const reactFlowInstance = useReactFlow(); 24 | 25 | // Access the store's actions 26 | const setOpenChatbotNodeId = useFlowStore( 27 | (state) => state.setOpenChatbotNodeId 28 | ); 29 | 30 | // Handler for delete button 31 | const handleDelete = () => { 32 | reactFlowInstance.setNodes((nds) => nds.filter((node) => node.id !== id)); 33 | reactFlowInstance.setEdges((eds) => 34 | eds.filter((edge) => edge.source !== id && edge.target !== id) 35 | ); 36 | // Also remove conversation from the store 37 | useFlowStore.getState().removeConversation(id); 38 | }; 39 | 40 | // Node style 41 | const buttonStyle = { 42 | padding: "8px 12px", 43 | backgroundColor: style?.backgroundColor || "#ffffff", 44 | border: selected ? "2px solid #555" : "1px solid #777", 45 | borderRadius: "5px", 46 | color: "#000", 47 | cursor: "pointer", 48 | textAlign: "center" as const, 49 | }; 50 | 51 | // Handle node click 52 | const handleNodeClick = (e: React.MouseEvent) => { 53 | e.stopPropagation(); 54 | setOpenChatbotNodeId(id); 55 | }; 56 | 57 | return ( 58 |
59 | {/* Toolbar */} 60 | {selected && ( 61 | 62 | 65 | 66 | )} 67 | 68 | {/* Handles for edge connections */} 69 | {targetPosition && ( 70 | 79 | )} 80 | {sourcePosition && ( 81 | 90 | )} 91 | 92 | {/* Button to open chatbot */} 93 | 100 |
101 | ); 102 | }; 103 | 104 | export default ChatbotNode; 105 | -------------------------------------------------------------------------------- /components/ContextDialog.tsx: -------------------------------------------------------------------------------- 1 | // components/ContextDialog.tsx 2 | "use client"; 3 | 4 | import React, { useState } from "react"; 5 | import { Card, CardHeader, CardContent, CardFooter } from "@/components/ui/card"; 6 | import { Button } from "@/components/ui/button"; 7 | import { Textarea } from "@/components/ui/textarea"; 8 | import { ScrollArea } from "@/components/ui/scroll-area"; 9 | import { X } from "lucide-react"; 10 | import { useFlowStore } from "@/storage/store"; 11 | import { Input } from "@/components/ui/input"; // Assuming you have a styled Input component 12 | 13 | export default function ContextDialog() { 14 | const { 15 | openContextNodeId, 16 | setOpenContextNodeId, 17 | nodes, 18 | addContextEntry, 19 | updateContextNodeLabel, 20 | } = useFlowStore(); 21 | 22 | const [inputValue, setInputValue] = useState(""); 23 | const [labelValue, setLabelValue] = useState(""); 24 | 25 | // Get the current node's context entries and label 26 | const currentNode = nodes.find((node) => node.id === openContextNodeId); 27 | const contextEntries = currentNode?.data?.contextEntries || []; 28 | 29 | // Initialize labelValue when we have a node 30 | React.useEffect(() => { 31 | if (currentNode && currentNode.data.label) { 32 | setLabelValue(currentNode.data.label); 33 | } 34 | }, [currentNode]); 35 | 36 | const handleAdd = () => { 37 | if (inputValue.trim() !== "" && openContextNodeId) { 38 | addContextEntry(openContextNodeId, inputValue.trim()); 39 | setInputValue(""); 40 | } 41 | }; 42 | 43 | const handleClose = () => { 44 | setOpenContextNodeId(null); 45 | }; 46 | 47 | const handleLabelSave = () => { 48 | if (openContextNodeId && labelValue.trim() !== "") { 49 | updateContextNodeLabel(openContextNodeId, labelValue.trim()); 50 | } 51 | }; 52 | 53 | if (!openContextNodeId) { 54 | return null; 55 | } 56 | 57 | return ( 58 |
59 | 60 | {/* Header */} 61 | 62 |

63 | {currentNode?.data?.label || "Context"} 64 |

65 | 68 |
69 | 70 | {/* Content */} 71 | 72 | {/* Rename context label */} 73 |
74 | setLabelValue(e.target.value)} 77 | placeholder="Rename context..." 78 | className="flex-1" 79 | /> 80 | 81 |
82 | 83 | {/* Display Context Entries */} 84 | 85 |
86 | {contextEntries.map((entry, index) => ( 87 |
91 | {entry} 92 |
93 | ))} 94 |
95 |
96 |
97 | 98 | 99 | {/* Textarea and Add Button */} 100 |
101 |