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
27 changes: 27 additions & 0 deletions sharedPrompts/my-app/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState, useEffect, useCallback } from 'react';
import { useQuery } from '@tanstack/react-query';
import { useQuery, keepPreviousData } from '@tanstack/react-query';
import { useDebounce } from '@/shared/hooks/useDebounce';
import { recommendPrompts } from '../api/prompt-builder.api';
import type { PromptBuilderState, UserOverrides } from '../types/prompt-builder.types';
Expand Down Expand Up @@ -35,16 +35,17 @@ export function usePromptRecommendations() {

const debouncedRawInput = useDebounce(state.rawInput, 500);

const { data: recommendations, isLoading, isError } = useQuery({
const { data: recommendations, isLoading, isFetching, isError } = useQuery({
queryKey: ['promptRecommendations', debouncedRawInput, state.category],
queryFn: () =>
recommendPrompts({
request_mode: 'SIMPLE',
category: state.category || undefined,
raw_input: debouncedRawInput,
}),
enabled: debouncedRawInput.length > 2,
enabled: debouncedRawInput.trim().length > 0,
staleTime: 1000 * 60 * 5,
placeholderData: keepPreviousData,
});

// Apply recommendations automatically for fields not overridden by the user
Expand All @@ -58,8 +59,10 @@ export function usePromptRecommendations() {
field: keyof PromptBuilderState['selectedAxes'],
recommendedValue?: string
) => {
if (!overrides[field] && recommendedValue && prev.selectedAxes[field] !== recommendedValue) {
nextState.selectedAxes[field] = recommendedValue;
// 백엔드가 추천해주지 않았으면 빈 문자열로 리셋 (사용자가 수동으로 선택하지 않은 경우)
const newVal = recommendedValue || '';
if (!overrides[field] && prev.selectedAxes[field] !== newVal) {
nextState.selectedAxes[field] = newVal;
hasChanges = true;
}
};
Expand All @@ -76,17 +79,15 @@ export function usePromptRecommendations() {
return hasChanges ? nextState : prev;
});

if (recommendations.axis_sources) {
setAxisSources(prev => {
const userSelected = Object.fromEntries(
Object.entries(prev).filter(([_, val]) => val === 'USER_SELECTED')
);
return {
...recommendations.axis_sources,
...userSelected,
};
});
}
setAxisSources(prev => {
const userSelected = Object.fromEntries(
Object.entries(prev).filter(([_, val]) => val === 'USER_SELECTED')
);
return {
...(recommendations.axis_sources || {}),
...userSelected,
};
});
}
}, [recommendations, overrides]);

Expand Down Expand Up @@ -120,7 +121,7 @@ export function usePromptRecommendations() {
overrides,
axisSources,
recommendations,
isLoading,
isLoading: isLoading || isFetching,
isError,
updateRawInput,
updateCategory,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/** POST /prompts/recommend — 백엔드 RecommendPromptRequest */
export interface RecommendPromptRequest {
request_mode: string; // e.g. 'SIMPLE'
request_mode: string; // RequestMode: SIMPLE | EXTRACTION | ADVANCED
category?: string;
intent?: string;
role_type?: string;
Expand All @@ -11,6 +12,7 @@ export interface RecommendPromptRequest {
raw_input: string;
}

/** POST /prompts/recommend 응답 — 백엔드 RecommendPromptResponse */
export interface RecommendPromptResponse {
request_mode: string;
category: string;
Expand All @@ -29,6 +31,7 @@ export interface RecommendPromptResponse {
default_selection?: string;
}

/** POST /prompts/generate/confirmed — 백엔드 ConfirmedGeneratePromptRequest */
export interface ConfirmedGeneratePromptRequest {
request_mode: string;
category: string;
Expand All @@ -39,24 +42,15 @@ export interface ConfirmedGeneratePromptRequest {
style?: string;
language?: string;
experience?: string;
input: string; // maps from raw_input
input: string;
json_schema?: string;
title?: string;
description?: string;
tags?: string[];
}

export interface UnifiedGeneratePromptResponse {
output: string;
resolved_category?: string;
resolved_intent?: string;
semantic_resolution_summary?: string;
axis_sources?: Record<string, string>;
recommendation_hints?: string[];
validation_warnings?: string[];
schema_failure_reasons?: string[];
quality_badges?: Array<{ name: string; description: string }>;
}
/** 통합 생성 응답은 prompt.types.UnifiedGeneratePromptResponse 사용 */
export type { UnifiedGeneratePromptResponse } from '@/features/prompt/types/prompt.types';

export interface PromptBuilderState {
rawInput: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { PromptInputPanel } from './PromptInputPanel';
import { SemanticSettingsPanel } from './SemanticSettingsPanel';
import { PromptResultPanel } from './PromptResultPanel';
import { usePromptRecommendations } from '../model/usePromptRecommendations';
import { generateConfirmedPrompt } from '../api/prompt-builder.api';
import type { UnifiedGeneratePromptResponse } from '../types/prompt-builder.types';
import { ArrowLeft } from 'lucide-react';
import {
TONE_DISPLAY_NAMES,
EXPERIENCE_DISPLAY_NAMES,
STYLE_DISPLAY_NAMES,
LANGUAGE_DISPLAY_NAMES,
} from '@/features/prompt/model/shared/enumDisplayNames';
import { ActionIntent, PromptCategory } from '@/features/prompt/types/prompt.types';
import { getActionTypesForCategory, getRoleTypesForCategory } from '@/features/prompt/types/category-mapping.types';

import { CreatePromptSuccess } from '@/features/prompt/ui/create/CreatePromptSuccess';

export const PromptBuilderPage: React.FC = () => {
const navigate = useNavigate();
const {
state,
overrides,
Expand All @@ -22,11 +35,29 @@ export const PromptBuilderPage: React.FC = () => {
const [generationResult, setGenerationResult] = useState<UnifiedGeneratePromptResponse | null>(null);
const [isGenerating, setIsGenerating] = useState(false);
const [generationError, setGenerationError] = useState<string | null>(null);
const [saved, setSaved] = useState(false);

const handleBack = () => {
navigate('/feed'); // or navigate(-1)
};

const fallbackIntents = Object.values(ActionIntent).filter(i => i !== 'DEBUG' && i !== 'DESIGN' && i !== 'CODE');
const fallbackRoles = state.category ? (getRoleTypesForCategory(state.category as PromptCategory) || []) : [];
const fallbackActions = state.category ? (getActionTypesForCategory(state.category as PromptCategory) || []) : [];

const validIntents = recommendations?.intent_candidates?.filter(Boolean) || [];
const validRoles = recommendations?.role_candidates?.filter(Boolean) || [];
const validActions = recommendations?.action_candidates?.filter(Boolean) || [];

const resolvedIntents = validIntents.length > 0 ? validIntents : fallbackIntents;
const resolvedRoles = validRoles.length > 0 ? validRoles : fallbackRoles;
const resolvedActions = validActions.length > 0 ? validActions : fallbackActions;

const handleGenerate = async () => {
setIsGenerating(true);
setGenerationError(null);
setGenerationResult(null);
setSaved(false);
try {
const response = await generateConfirmedPrompt({
request_mode: 'SIMPLE',
Expand All @@ -41,6 +72,7 @@ export const PromptBuilderPage: React.FC = () => {
input: state.rawInput.trim(),
});
setGenerationResult(response);
setSaved(true);
} catch (error) {
console.error('Failed to generate prompt', error);
setGenerationError('Failed to generate prompt. Please try again.');
Expand All @@ -53,66 +85,91 @@ export const PromptBuilderPage: React.FC = () => {
state.rawInput.trim().length > 0 &&
state.selectedAxes.intent.trim().length > 0;

return (
<div className="container mx-auto max-w-7xl px-4 py-8 min-h-screen bg-gray-50/50">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 tracking-tight">AI Prompt Builder</h1>
<p className="mt-2 text-gray-600">Tell us what you want to create, and we'll automatically suggest the best settings.</p>
</div>
if (saved) {
return <CreatePromptSuccess onReset={() => {
setSaved(false);
setGenerationResult(null);
}} />;
}

<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 h-full">
{/* Left Column: Input & Settings */}
<div className="lg:col-span-7 flex flex-col gap-6">
<PromptInputPanel
rawInput={state.rawInput}
onRawInputChange={updateRawInput}
category={state.category}
onCategoryChange={updateCategory}
/>

{isRecommendError && (
<div
role="alert"
aria-live="assertive"
className="p-3 text-sm text-amber-700 bg-amber-50 rounded-md border border-amber-200"
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-cyan-50">
<header className="bg-white border-b border-blue-100">
<div className="max-w-7xl mx-auto px-4 py-6">
<div className="flex items-center gap-3">
<button
type="button"
onClick={handleBack}
aria-label="뒤로가기"
className="p-2 hover:bg-blue-50 rounded-xl transition-colors"
>
추천을 불러오는 데 실패했습니다. 수동으로 설정을 선택해주세요.
</div>
)}

<SemanticSettingsPanel
selectedAxes={state.selectedAxes}
overrides={overrides}
axisSources={axisSources}
candidates={{
intentCandidates: recommendations?.intent_candidates || [],
roleCandidates: recommendations?.role_candidates || [],
actionCandidates: recommendations?.action_candidates || [],
}}
onUpdateField={updateField}
isLoading={isRecommending}
/>
<ArrowLeft className="w-5 h-5 text-neutral-600" />
</button>
<h1 className="text-2xl font-bold text-neutral-900">AI Prompt Builder</h1>
<p className="ml-4 mt-1 text-gray-600">Tell us what you want to create, and we'll automatically suggest the best settings.</p>
</div>
</div>
</header>

<main className="container mx-auto max-w-7xl px-4 py-8 h-full">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 h-full">
{/* Left Column: Input & Settings */}
<div className="lg:col-span-7 flex flex-col gap-6">
<PromptInputPanel
rawInput={state.rawInput}
onRawInputChange={updateRawInput}
category={state.category}
onCategoryChange={updateCategory}
/>

{isRecommendError && (
<div
role="alert"
aria-live="assertive"
className="p-3 text-sm text-amber-700 bg-amber-50 rounded-md border border-amber-200"
>
추천을 불러오는 데 실패했습니다. 수동으로 설정을 선택해주세요.
</div>
)}

{/* Right Column: Preview & Output */}
<div className="lg:col-span-5 h-[calc(100vh-12rem)] sticky top-8">
{generationError && (
<div
role="alert"
aria-live="assertive"
className="mb-4 p-4 text-sm text-red-700 bg-red-100 rounded-md border border-red-200"
>
{generationError}
</div>
)}
<PromptResultPanel
generationResult={generationResult}
isLoading={isGenerating}
onGenerate={handleGenerate}
canGenerate={canGenerate}
/>
<SemanticSettingsPanel
selectedAxes={state.selectedAxes}
overrides={overrides}
axisSources={axisSources}
candidates={{
intentCandidates: resolvedIntents,
roleCandidates: resolvedRoles,
actionCandidates: resolvedActions,
toneCandidates: Object.keys(TONE_DISPLAY_NAMES),
styleCandidates: Object.keys(STYLE_DISPLAY_NAMES),
languageCandidates: Object.keys(LANGUAGE_DISPLAY_NAMES),
experienceCandidates: Object.keys(EXPERIENCE_DISPLAY_NAMES),
}}
onUpdateField={updateField}
isLoading={isRecommending}
/>
</div>

{/* Right Column: Preview & Output */}
<div className="lg:col-span-5 h-[calc(100vh-12rem)] sticky top-8">
{generationError && (
<div
role="alert"
aria-live="assertive"
className="mb-4 p-4 text-sm text-red-700 bg-red-100 rounded-md border border-red-200"
>
{generationError}
</div>
)}
<PromptResultPanel
generationResult={generationResult}
isLoading={isGenerating}
onGenerate={handleGenerate}
canGenerate={canGenerate}
/>
</div>
</div>
</div>
</main>
</div>
);
};
Loading