diff --git a/backend/src/generator/generator.service.ts b/backend/src/generator/generator.service.ts index bbb8163d..2905b28c 100644 --- a/backend/src/generator/generator.service.ts +++ b/backend/src/generator/generator.service.ts @@ -1,7 +1,6 @@ import OpenAI from 'openai'; -import logger from '../utils/logger.js'; -import dotenv from 'dotenv'; import { cbManager } from '../lib/circuit-breaker/CircuitBreakerManager.js'; +import logger from '../utils/logger.js'; // dotenv.config(); // Skip in Docker Compose - use environment variables instead @@ -34,24 +33,26 @@ export class GeneratorService { async generateProjectIdea( theme: string, techStack: string[], - difficulty: string + difficulty: string, + customRpcUrl?: string ): Promise { return this.breaker.execute( async () => { const prompt = ` As an expert Web3 and Software Architect, generate a unique and innovative hackathon project idea. - + Theme: ${theme} Technology Stack: ${techStack.join(', ')} Target Difficulty: ${difficulty} - + ${customRpcUrl ? `\nIf this project will interact with a blockchain, prefer using the following RPC endpoint: ${customRpcUrl}` : ''} + Return the response in a structured JSON format with the following keys: - title: A catchy name for the project. - description: A detailed description of the project and its value proposition. - keyFeatures: An array of 3-5 core functionalities. - recommendedTech: An array of tools and libraries that would be useful. - difficulty: The suggested level (Beginner, Intermediate, or Advanced). - + Ensure the idea is practical for a 48-hour hackathon but still innovative. `; diff --git a/backend/src/routes/generator/generator.routes.ts b/backend/src/routes/generator/generator.routes.ts index 71b5b6ca..129f9f12 100644 --- a/backend/src/routes/generator/generator.routes.ts +++ b/backend/src/routes/generator/generator.routes.ts @@ -1,10 +1,10 @@ // @ts-nocheck +import { randomUUID } from 'crypto'; import { Request, Response, Router } from 'express'; import { GeneratorService } from '../../generator/generator.service.js'; -import logger from '../../utils/logger.js'; import { getRandomProjectIdea, mockProjectIdeas } from '../../generator/mockData.js'; -import { randomUUID } from 'crypto'; import { storageService } from '../../services/storage/index.js'; +import logger from '../../utils/logger.js'; const router = Router(); const generatorService = new GeneratorService(); @@ -21,7 +21,21 @@ const slugify = (value: string): string => */ router.post('/generate', async (req: Request, res: Response) => { try { - const { theme, techStack, difficulty, persistToStorage, queuedPersist } = req.body; + const { theme, techStack, difficulty, persistToStorage, queuedPersist, customRpcUrl } = req.body; + + // Validate optional custom RPC URL + if (customRpcUrl) { + try { + const url = new URL(customRpcUrl); + if (url.protocol !== 'http:' && url.protocol !== 'https:') { + res.status(400).json({ error: 'customRpcUrl must use http or https' }); + return; + } + } catch (err) { + res.status(400).json({ error: 'customRpcUrl is not a valid URL' }); + return; + } + } if (!theme || !techStack || !difficulty) { res.status(400).json({ error: 'Theme, techStack, and difficulty are required' }); @@ -29,8 +43,13 @@ router.post('/generate', async (req: Request, res: Response) => { } // Try AI generation first, fallback to mock data if it fails - try { - const projectIdea = await generatorService.generateProjectIdea(theme, techStack, difficulty); + try { + const projectIdea = await generatorService.generateProjectIdea( + theme, + techStack, + difficulty, + customRpcUrl + ); if (persistToStorage) { const projectId = `${slugify(theme)}-${Date.now()}-${randomUUID().slice(0, 8)}`; diff --git a/backend/tests/generator.service.test.ts b/backend/tests/generator.service.test.ts new file mode 100644 index 00000000..a668c163 --- /dev/null +++ b/backend/tests/generator.service.test.ts @@ -0,0 +1,57 @@ +import { jest } from '@jest/globals'; + +const mockCreate = jest.fn(); + +jest.mock('openai', () => { + return { + default: jest.fn().mockImplementation(() => ({ + chat: { + completions: { + create: mockCreate, + }, + }, + })), + }; +}); + +describe('GeneratorService - custom RPC integration', () => { + beforeEach(() => { + mockCreate.mockClear(); + }); + + it('passes customRpcUrl into the AI prompt when provided', async () => { + process.env.OPENAI_API_KEY = 'test-key'; + + const mockResponse = { + choices: [ + { + message: { + content: JSON.stringify({ + title: 'RPC Test Project', + description: 'Test description', + keyFeatures: ['f1'], + recommendedTech: ['Node.js'], + difficulty: 'Intermediate', + }), + }, + }, + ], + }; + + mockCreate.mockResolvedValue(mockResponse as never); + + // Dynamic import to ensure mock is applied + const mod = await import('../src/generator/generator.service.js'); + const GeneratorService = mod.GeneratorService; + const svc = new GeneratorService(); + + const customRpc = 'https://custom-rpc.example:443'; + const result = await svc.generateProjectIdea('Theme', ['React'], 'Intermediate', customRpc); + + expect(mockCreate).toHaveBeenCalled(); + const calledArg = mockCreate.mock.calls[0][0]; + const userMessage = calledArg.messages[1].content; + expect(userMessage).toContain(customRpc); + expect(result).toHaveProperty('title', 'RPC Test Project'); + }); +}); diff --git a/frontend/HACKATHON_IDEA_GENERATOR_README.md b/frontend/HACKATHON_IDEA_GENERATOR_README.md index f58e5f98..ad2ac967 100644 --- a/frontend/HACKATHON_IDEA_GENERATOR_README.md +++ b/frontend/HACKATHON_IDEA_GENERATOR_README.md @@ -30,6 +30,10 @@ existing [`generatorAPI`](src/lib/api.ts) in `src/lib/api.ts` — no new client introduced. `buildGeneratorParams()` maps the UI filters to the request shape the endpoint already accepts (`{ theme, techStack, difficulty }`). +The backend endpoint also accepts an optional `customRpcUrl` string in the +request body; when provided the AI prompt will be instructed to prefer that RPC +endpoint for any blockchain interactions mentioned in the generated idea. + ## Filtering - **Difficulty** — `Beginner | Intermediate | Advanced` (matches `ProjectIdea`).