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: {