diff --git a/prompts/00-product-names-analysis.txt b/prompts/00-product-names-analysis.txt new file mode 100644 index 0000000..e293b7e --- /dev/null +++ b/prompts/00-product-names-analysis.txt @@ -0,0 +1,42 @@ +You are a branding consultant helping the user come up with potential names for their product. Based on the problem and target audience they described, suggest 5 product names that capture the essence of their product and core functionality. + +For each name: +1. Ensure it is short and catchy (ideally 1-3 words). +2. Provide a brief description of what the name means and why it fits the product. +3. Match the tone of the names to the product's target audience (e.g., playful, professional, or neutral). +4. Include a catchy tagline that encapsulates the essence of the product. +5. Provide insight on how the name resonates with the intended target audience. +6. Suggest similar names for users to consider. +7. Briefly describe the branding potential of the name, including aspects like logo potential or marketability. + +### Expected Responses: + +```json +{ + "product_names": [ + { + "product_name": "Name1", + "domains": ["name1.com", "name1app.com"], + "why": "A brief description of why this product name is great.", + "tagline": "Catchy tagline that sums up the product.", + "target_audience_insight": "This name appeals to young professionals looking for innovative solutions.", + "similar_names": ["Name1Plus", "Name1Pro"], + "branding_potential": "The name has strong branding potential with a memorable sound and visual appeal." + }, + { + "product_name": "Name2", + "domains": ["name2.com", "name2app.com"], + "why": "A brief description of why this product name is great.", + "tagline": "A catchy tagline that captures the product's essence.", + "target_audience_insight": "Appeals to tech-savvy users seeking efficiency.", + "similar_names": ["Name2Lite", "Name2X"], + "branding_potential": "This name could be effectively branded with modern visuals." + } + ] +} +``` + +### Guidelines: +- Friendly Tone: Use a conversational tone, as if you're chatting with a friend. +- Avoid Jargon: Skip formal, sales-like language or buzzwords such as "streamline," "enhance," "tailored," "leverage," "thrilled," etc. +- Ensure the language is clear, practical, and builds confidence in the product's value. diff --git a/src/app/ideas/[id]/IdeaAnalysisReport.tsx b/src/app/ideas/[id]/IdeaAnalysisReport.tsx index 6502b5b..6af6486 100644 --- a/src/app/ideas/[id]/IdeaAnalysisReport.tsx +++ b/src/app/ideas/[id]/IdeaAnalysisReport.tsx @@ -1,5 +1,6 @@ 'use client' +import Link from 'next/link' import { useRouter } from 'next/navigation' import React, { useEffect, useState } from 'react' import FetchingDataMessage from '@/components/FetchingDataMessage' @@ -55,6 +56,15 @@ interface Props { } differentiationSuggestions: string[] } | null + productNames: Array<{ + productName: string + domains: string[] + why: string + tagline: string + targetAudienceInsight: string + similarNames: string[] + brandingPotential: string + }> | null } } @@ -614,23 +624,79 @@ export const IdeaAnalysisReport = ({ data }: Props) => {
toggleSection('potentialProductNames')} isExpanded={expandedSections.potentialProductNames} sectionId="potentialProductNames" > - This Week: Potential Product Names + Potential Product Names {expandedSections.potentialProductNames && ( -
- - Here, we brainstorm some catchy names for your product. A good - name can leave a lasting impression and make your product more - memorable. This is a fun part of the process that allows you to - think creatively! - -
+ <> +
+ + Here, we brainstorm some catchy names for your product. A good + name can leave a lasting impression and make your product more + memorable. This is a fun part of the process that allows you to + think creatively! + +
+ + {data.productNames !== null ? ( + <> + {data.productNames.map((productName, idx) => ( +
+
+ + {productName.why} {productName.targetAudienceInsight} + + +

+ Branding Potential: +

+ {productName.brandingPotential} + +

+ Potential Domains: +

+ +
    + {productName.domains.map((item, index) => ( +
  • + + {item} + +
  • + ))} +
+ +

+ Similar Product Names: +

+ +
+
+ ))} + + ) : ( + + )} + )}
diff --git a/src/idea/adapters/IdeaRepositorySQLite.ts b/src/idea/adapters/IdeaRepositorySQLite.ts index 4fe0363..4966bbe 100644 --- a/src/idea/adapters/IdeaRepositorySQLite.ts +++ b/src/idea/adapters/IdeaRepositorySQLite.ts @@ -5,6 +5,7 @@ import { Repository } from '@/idea/domain/Repository' import { TargetAudience } from '@/idea/domain/TargetAudience' import { ValueProposition } from '@/idea/domain/ValueProposition' import { prisma } from '@/lib/prisma' +import { ProductName } from '../domain/ProductName' import type { PrismaClient } from '@prisma/client/extension' type UpdateFn = (idea: Idea) => Idea @@ -121,6 +122,27 @@ export class IdeaRepositorySQLite implements Repository { }, }) } + + const productNames = updatedIdea.getProductNames() + if (productNames) { + await prisma.ideaContent.upsert({ + where: { + ideaId_key: { + ideaId: id, + key: 'product_names', + }, + }, + create: { + ideaId: id, + key: 'product_names', + value: JSON.stringify(productNames), + }, + update: { + value: JSON.stringify(productNames), + updatedAt: new Date(), + }, + }) + } }) } @@ -286,4 +308,45 @@ export class IdeaRepositorySQLite implements Repository { data.differentiationSuggestions ) } + + async getProductNamesByIdeaId(ideaId: string): Promise { + const productNamesModel = await prisma.ideaContent.findUnique({ + where: { + ideaId_key: { + ideaId: ideaId, + key: 'product_names', + }, + }, + }) + + if (!productNamesModel) { + return null + } + + interface productName { + productName: string + domains: string[] + why: string + tagline: string + targetAudienceInsight: string + similarNames: string[] + brandingPotential: string + } + + type productNames = productName[] + + const data = JSON.parse(productNamesModel.value) as productNames + + return data.map((product) => + ProductName.New( + product.productName, + product.domains, + product.why, + product.tagline, + product.targetAudienceInsight, + product.similarNames, + product.brandingPotential + ) + ) + } } diff --git a/src/idea/adapters/OpenAIService/PotentialNamesEvaluator.ts b/src/idea/adapters/OpenAIService/PotentialNamesEvaluator.ts new file mode 100644 index 0000000..687fa8d --- /dev/null +++ b/src/idea/adapters/OpenAIService/PotentialNamesEvaluator.ts @@ -0,0 +1,122 @@ +import OpenAI from 'openai' +import { z } from 'zod' +import { getPromptContent } from '@/lib/prompts' + +interface PotentialName { + productName: string + domains: string[] + why: string + tagline: string + targetAudienceInsight: string + similarNames: string[] + brandingPotential: string +} + +type Evaluation = PotentialName[] + +interface TargetAudience { + segment: string + description: string + challenges: string[] +} + +const ResponseSchema = z.object({ + product_names: z.array( + z.object({ + product_name: z.string(), + domains: z.array(z.string()), + why: z.string(), + tagline: z.string(), + target_audience_insight: z.string(), + similar_names: z.array(z.string()), + branding_potential: z.string(), + }) + ), +}) + +export class PotentialNamesEvaluator { + private readonly openai: OpenAI + + constructor(apiKey: string) { + this.openai = new OpenAI({ + apiKey: apiKey, + }) + } + + async evaluatePotentialNames( + problem: string, + marketExistence: string, + targetAudiences: TargetAudience[] + ): Promise { + const promptContent = getPromptContent('00-product-names-analysis') + + if (!promptContent) { + throw new Error('Prompt content not found') + } + + const response = await this.openai.chat.completions.create({ + model: 'gpt-4o-mini', + messages: [ + { + role: 'system', + content: [ + { + type: 'text', + text: promptContent.trim(), + }, + ], + }, + { + role: 'user', + content: [ + { + type: 'text', + text: `Here is the problem my product aims to solve: """ +${problem.trim()}""" + +Also I have a market existence research: """ +${marketExistence.trim()}""" + +Here are my segments: """ +${targetAudiences + .map((targetAudience, idx) => { + let content = '' + + content += `Segment ${idx + 1}: ${targetAudience.segment}\n` + content += `Description: ${targetAudience.description}\n` + content += `Challenges:\n${targetAudience.challenges.join('; ')}\n\n` + + return content + }) + .join('\n\n')} +"""`, + }, + ], + }, + ], + // For most factual use cases such as data extraction, and truthful Q&A, the temperature of 0 is best. + // https://help.openai.com/en/articles/6654000-best-practices-for-prompt-engineering-with-the-openai-api + temperature: 0.7, + max_tokens: 2000, + response_format: { + type: 'json_object', + }, + }) + + // TODO: Store response.usage for better analysis + + const content = response.choices[0].message.content ?? '' + + const analysis = ResponseSchema.parse(JSON.parse(content)) + + return analysis.product_names.map((product) => ({ + productName: product.product_name, + domains: product.domains, + why: product.why, + tagline: product.tagline, + targetAudienceInsight: product.target_audience_insight, + similarNames: product.similar_names, + brandingPotential: product.branding_potential, + })) + } +} diff --git a/src/idea/app/queries/GetIdea.ts b/src/idea/app/queries/GetIdea.ts index d616e4a..ecdd722 100644 --- a/src/idea/app/queries/GetIdea.ts +++ b/src/idea/app/queries/GetIdea.ts @@ -1,6 +1,7 @@ import { Idea } from '@/idea/domain/Aggregate' import { CompetitorAnalysis } from '@/idea/domain/CompetitorAnalysis' import { MarketAnalysis } from '@/idea/domain/MarketAnalysis' +import { ProductName } from '@/idea/domain/ProductName' import { TargetAudience } from '@/idea/domain/TargetAudience' import { ValueProposition } from '@/idea/domain/ValueProposition' @@ -51,6 +52,15 @@ interface FullIdeaDTO { } differentiationSuggestions: string[] } | null + productNames: Array<{ + productName: string + domains: string[] + why: string + tagline: string + targetAudienceInsight: string + similarNames: string[] + brandingPotential: string + }> | null } interface ReadModel { @@ -61,6 +71,7 @@ interface ReadModel { getCompetitorAnalysisByIdeaId( ideaId: string ): Promise + getProductNamesByIdeaId(ideaId: string): Promise } export class GetIdeaHandler { @@ -88,6 +99,8 @@ export class GetIdeaHandler { const competitorAnalysis = await this.readModel.getCompetitorAnalysisByIdeaId(query.id) + const productNames = await this.readModel.getProductNamesByIdeaId(query.id) + return { id: idea.getId().getValue(), problem: idea.getProblem().getValue(), @@ -126,6 +139,17 @@ export class GetIdeaHandler { competitorAnalysis.getDifferentiationSuggestions(), } : null, + productNames: productNames + ? productNames.map((product) => ({ + productName: product.getProductName(), + domains: product.getDomains(), + why: product.getWhy(), + tagline: product.getTagline(), + targetAudienceInsight: product.getTargetAudienceInsight(), + similarNames: product.getSimilarNames(), + brandingPotential: product.getBrandingPotential(), + })) + : null, } } } diff --git a/src/idea/domain/Aggregate.ts b/src/idea/domain/Aggregate.ts index 3b7608d..127eb1f 100644 --- a/src/idea/domain/Aggregate.ts +++ b/src/idea/domain/Aggregate.ts @@ -1,6 +1,7 @@ import { CompetitorAnalysis } from '@/idea/domain/CompetitorAnalysis' import { MarketAnalysis } from '@/idea/domain/MarketAnalysis' import { Problem } from '@/idea/domain/Problem' +import { ProductName } from '@/idea/domain/ProductName' import { TargetAudience } from '@/idea/domain/TargetAudience' import { ValueProposition } from '@/idea/domain/ValueProposition' import { Identity } from '@/shared/Identity' @@ -15,6 +16,7 @@ export class Idea { private valueProposition: ValueProposition | null = null private marketAnalysis: MarketAnalysis | null = null private competitorAnalysis: CompetitorAnalysis | null = null + private productNames: ProductName[] = [] private migrated: boolean = false private constructor( @@ -60,6 +62,10 @@ export class Idea { this.competitorAnalysis = competitorAnalysis } + public addProductName(productName: ProductName): void { + this.productNames.push(productName) + } + public finalizeMigration(): void { if (this.migrated) { throw new Error('Idea was migrated') @@ -100,6 +106,10 @@ export class Idea { return this.competitorAnalysis } + public getProductNames(): ProductName[] { + return this.productNames + } + public isMigrated(): boolean { return this.migrated } diff --git a/src/idea/domain/ProductName.ts b/src/idea/domain/ProductName.ts new file mode 100644 index 0000000..8e52e2c --- /dev/null +++ b/src/idea/domain/ProductName.ts @@ -0,0 +1,75 @@ +export class ProductName { + private readonly productName: string + private readonly domains: string[] + private readonly why: string + private readonly tagline: string + private readonly targetAudienceInsight: string + private readonly similarNames: string[] + private readonly brandingPotential: string + + private constructor( + productName: string, + domains: string[], + why: string, + tagline: string, + targetAudienceInsight: string, + similarNames: string[], + brandingPotential: string + ) { + this.productName = productName + this.domains = domains + this.why = why + this.tagline = tagline + this.targetAudienceInsight = targetAudienceInsight + this.similarNames = similarNames + this.brandingPotential = brandingPotential + } + + static New( + productName: string, + domains: string[], + why: string, + tagline: string, + targetAudienceInsight: string, + similarNames: string[], + brandingPotential: string + ): ProductName { + return new ProductName( + productName, + domains, + why, + tagline, + targetAudienceInsight, + similarNames, + brandingPotential + ) + } + + public getProductName(): string { + return this.productName + } + + public getDomains(): string[] { + return this.domains + } + + public getWhy(): string { + return this.why + } + + public getTagline(): string { + return this.tagline + } + + public getTargetAudienceInsight(): string { + return this.targetAudienceInsight + } + + public getSimilarNames(): string[] { + return this.similarNames + } + + public getBrandingPotential(): string { + return this.brandingPotential + } +} diff --git a/src/idea/events/subscribers/PotentialNamesEvaluationSubscriber.ts b/src/idea/events/subscribers/PotentialNamesEvaluationSubscriber.ts new file mode 100644 index 0000000..25cb6a1 --- /dev/null +++ b/src/idea/events/subscribers/PotentialNamesEvaluationSubscriber.ts @@ -0,0 +1,81 @@ +import { Idea } from '@/idea/domain/Aggregate' + +import { ProductName } from '@/idea/domain/ProductName' +import { Repository } from '@/idea/domain/Repository' +import { IdeaCreated } from '@/idea/domain/events/IdeaCreated' +import { EventHandler } from '@/idea/events/EventHandler' + +interface PotentialName { + productName: string + domains: string[] + why: string + tagline: string + targetAudienceInsight: string + similarNames: string[] + brandingPotential: string +} + +type Evaluation = PotentialName[] + +interface TargetAudience { + segment: string + description: string + challenges: string[] +} + +export interface AIService { + evaluatePotentialNames( + problem: string, + marketExistence: string, + targetAudiences: TargetAudience[] + ): Promise +} + +export class PotentialNamesEvaluationSubscriber implements EventHandler { + constructor( + private readonly repository: Repository, + private readonly aiService: AIService + ) {} + + async handle(event: IdeaCreated): Promise { + const idea = await this.repository.getById(event.payload.id) + + if (!idea) { + throw new Error(`Unable to get idea by ID: ${event.payload.id}`) + } + + const targetAudiences = await this.repository.getTargetAudiencesByIdeaId( + idea.getId().getValue() + ) + + const audiences = targetAudiences.map((targetAudience) => ({ + segment: targetAudience.getSegment(), + description: targetAudience.getDescription(), + challenges: targetAudience.getChallenges(), + })) + + const evaluation = await this.aiService.evaluatePotentialNames( + idea.getProblem().getValue(), + idea.getMarketExistence(), + audiences + ) + + await this.repository.updateIdea(event.payload.id, (idea): Idea => { + evaluation.forEach((product) => { + idea.addProductName( + ProductName.New( + product.productName, + product.domains, + product.why, + product.tagline, + product.targetAudienceInsight, + product.similarNames, + product.brandingPotential + ) + ) + }) + + return idea + }) + } +} diff --git a/src/idea/service/Service.ts b/src/idea/service/Service.ts index 89e9a05..0376c58 100644 --- a/src/idea/service/Service.ts +++ b/src/idea/service/Service.ts @@ -3,6 +3,7 @@ import { EventBusInMemory } from '@/idea/adapters/EventBusInMemory' import { IdeaRepositorySQLite } from '@/idea/adapters/IdeaRepositorySQLite' import { CompetitorAnalysisEvaluator } from '@/idea/adapters/OpenAIService/CompetitorAnalysisEvaluator' import { MarketAnalysisEvaluator } from '@/idea/adapters/OpenAIService/MarketAnalysisEvaluator' +import { PotentialNamesEvaluator } from '@/idea/adapters/OpenAIService/PotentialNamesEvaluator' import { TargetAudienceEvaluator } from '@/idea/adapters/OpenAIService/TargetAudienceEvaluator' import { ValuePropositionEvaluator } from '@/idea/adapters/OpenAIService/ValuePropositionEvaluator' import { Application } from '@/idea/app/App' @@ -10,6 +11,7 @@ import { MakeReservationHandler } from '@/idea/app/commands/MakeReservation' import { GetIdeaHandler } from '@/idea/app/queries/GetIdea' import { CompetitorAnalysisEvaluationSubscriber } from '@/idea/events/subscribers/CompetitorAnalysisEvaluationSubscriber' import { MarketAnalysisEvaluationSubscriber } from '@/idea/events/subscribers/MarketAnalysisEvaluationSubscriber' +import { PotentialNamesEvaluationSubscriber } from '@/idea/events/subscribers/PotentialNamesEvaluationSubscriber' import { TargetAudienceEvaluationSubscriber } from '@/idea/events/subscribers/TargetAudienceEvaluationSubscriber' import { ValuePropositionEvaluationSubscriber } from '@/idea/events/subscribers/ValuePropositionEvaluationSubscriber' import { env } from '@/lib/env' @@ -43,10 +45,17 @@ const registerApp = (): Application => { new CompetitorAnalysisEvaluator(env.OPENAI_API_KEY) ) + const potentialNamesEvaluationSubscriber = + new PotentialNamesEvaluationSubscriber( + ideaRepository, + new PotentialNamesEvaluator(env.OPENAI_API_KEY) + ) + eventBus.subscribe('IdeaCreated', targetAudienceEvaluationSubscriber) eventBus.subscribe('IdeaCreated', valuePropositionEvaluationSubscriber) eventBus.subscribe('IdeaCreated', marketAnalysisEvaluationSubscriber) eventBus.subscribe('IdeaCreated', competitorAnalysisEvaluationSubscriber) + eventBus.subscribe('IdeaCreated', potentialNamesEvaluationSubscriber) return { Commands: {