Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions backend/src/generator/generator.service.ts
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -34,24 +33,26 @@ export class GeneratorService {
async generateProjectIdea(
theme: string,
techStack: string[],
difficulty: string
difficulty: string,
customRpcUrl?: string
): Promise<ProjectIdea> {
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.
`;

Expand Down
29 changes: 24 additions & 5 deletions backend/src/routes/generator/generator.routes.ts
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -21,16 +21,35 @@ 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' });
return;
}

// 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)}`;
Expand Down
57 changes: 57 additions & 0 deletions backend/tests/generator.service.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
4 changes: 4 additions & 0 deletions frontend/HACKATHON_IDEA_GENERATOR_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
Expand Down