mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-11-02 18:19:44 +00:00
Update
This commit is contained in:
1 parent
8b73781272
commit
75509b0e45
41 files changed
+5896
-2931
No files matched your search
@@ -0,0 +1,401 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
generatePack,
|
||||
generateQuestionsWithCouncil,
|
||||
suggestCategories,
|
||||
suggestPacks,
|
||||
getCouncilConfig,
|
||||
getModel,
|
||||
type GeneratedPack,
|
||||
type GeneratedQuestion,
|
||||
type GeneratedCategoryIdea,
|
||||
type GeneratedPackIdea,
|
||||
type ToneLevel,
|
||||
type QuestionReview,
|
||||
type CouncilGenerationResult,
|
||||
type GenerationCandidate,
|
||||
type CherryPickResult,
|
||||
type CouncilSelectionMode,
|
||||
type GenerationProgress,
|
||||
} from '@/lib/openai';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
// =============================================================================
|
||||
// Types
|
||||
// =============================================================================
|
||||
|
||||
export type GeneratorType = 'pack' | 'questions' | 'category-ideas' | 'category-pack-ideas';
|
||||
|
||||
export interface GeneratorContext {
|
||||
categoryName?: string;
|
||||
packName?: string;
|
||||
packDescription?: string | null;
|
||||
existingCategories?: string[];
|
||||
existingPacks?: string[];
|
||||
existingQuestions?: string[];
|
||||
isExplicit?: boolean;
|
||||
}
|
||||
|
||||
export interface GenerationConfig {
|
||||
count: number;
|
||||
tone: ToneLevel;
|
||||
crudeLang: boolean;
|
||||
inspiration: string;
|
||||
}
|
||||
|
||||
export interface GenerationResults {
|
||||
pack: GeneratedPack | null;
|
||||
questions: GeneratedQuestion[];
|
||||
categoryIdeas: GeneratedCategoryIdea[];
|
||||
packIdeas: GeneratedPackIdea[];
|
||||
}
|
||||
|
||||
export interface ReviewState {
|
||||
reviews: QuestionReview[];
|
||||
summary: CouncilGenerationResult['summary'];
|
||||
metadata: CouncilGenerationResult['metadata'] | null;
|
||||
selectedGeneratorIndex: number | null;
|
||||
allCandidates: GenerationCandidate[] | null;
|
||||
cherryPickResult: CherryPickResult | null;
|
||||
selectionMode: CouncilSelectionMode;
|
||||
}
|
||||
|
||||
export type ReviewFilter = 'all' | 'pass' | 'flag' | 'reject';
|
||||
|
||||
// =============================================================================
|
||||
// Hook
|
||||
// =============================================================================
|
||||
|
||||
export function useAiGeneration(
|
||||
type: GeneratorType,
|
||||
context?: GeneratorContext
|
||||
) {
|
||||
// Loading state
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [progress, setProgress] = useState<GenerationProgress | null>(null);
|
||||
|
||||
// Config state
|
||||
const [config, setConfig] = useState<GenerationConfig>({
|
||||
count: 10,
|
||||
tone: context?.isExplicit ? 5 : 3,
|
||||
crudeLang: false,
|
||||
inspiration: '',
|
||||
});
|
||||
|
||||
// Results state
|
||||
const [results, setResults] = useState<GenerationResults>({
|
||||
pack: null,
|
||||
questions: [],
|
||||
categoryIdeas: [],
|
||||
packIdeas: [],
|
||||
});
|
||||
|
||||
// Review state
|
||||
const [reviewState, setReviewState] = useState<ReviewState>({
|
||||
reviews: [],
|
||||
summary: null,
|
||||
metadata: null,
|
||||
selectedGeneratorIndex: null,
|
||||
allCandidates: null,
|
||||
cherryPickResult: null,
|
||||
selectionMode: 'whole_set',
|
||||
});
|
||||
|
||||
// Selection state
|
||||
const [selectedIndices, setSelectedIndices] = useState<Set<number>>(new Set());
|
||||
const [filter, setFilter] = useState<ReviewFilter>('all');
|
||||
|
||||
// Source model tracking
|
||||
const [sourceModel, setSourceModel] = useState<string | null>(null);
|
||||
|
||||
// Previous suggestions tracking (avoid repetition on regenerate)
|
||||
const [previousNames, setPreviousNames] = useState<{
|
||||
packs: string[];
|
||||
categories: string[];
|
||||
packIdeas: string[];
|
||||
}>({ packs: [], categories: [], packIdeas: [] });
|
||||
|
||||
// Council config
|
||||
const councilConfig = getCouncilConfig();
|
||||
|
||||
// Update tone when context changes
|
||||
useEffect(() => {
|
||||
if (type === 'questions') {
|
||||
setConfig(prev => ({
|
||||
...prev,
|
||||
tone: context?.isExplicit ? 5 : 3,
|
||||
}));
|
||||
}
|
||||
}, [context?.isExplicit, type]);
|
||||
|
||||
// Config setters
|
||||
const setCount = useCallback((count: number) => {
|
||||
setConfig(prev => ({ ...prev, count }));
|
||||
}, []);
|
||||
|
||||
const setTone = useCallback((tone: ToneLevel) => {
|
||||
setConfig(prev => ({ ...prev, tone }));
|
||||
}, []);
|
||||
|
||||
const setCrudeLang = useCallback((crudeLang: boolean) => {
|
||||
setConfig(prev => ({ ...prev, crudeLang }));
|
||||
}, []);
|
||||
|
||||
const setInspiration = useCallback((inspiration: string) => {
|
||||
setConfig(prev => ({ ...prev, inspiration }));
|
||||
}, []);
|
||||
|
||||
// Helper functions
|
||||
const getReviewForIndex = useCallback((index: number): QuestionReview | undefined => {
|
||||
return reviewState.reviews.find(r => r.index === index);
|
||||
}, [reviewState.reviews]);
|
||||
|
||||
const getFilteredQuestionIndices = useCallback((): number[] => {
|
||||
if (results.questions.length === 0) return [];
|
||||
if (filter === 'all' || reviewState.reviews.length === 0) {
|
||||
return results.questions.map((_, i) => i);
|
||||
}
|
||||
return reviewState.reviews
|
||||
.filter(r => r.verdict === filter)
|
||||
.map(r => r.index);
|
||||
}, [results.questions, reviewState.reviews, filter]);
|
||||
|
||||
// Selection handlers
|
||||
const toggleSelection = useCallback((index: number) => {
|
||||
setSelectedIndices(prev => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(index)) {
|
||||
next.delete(index);
|
||||
} else {
|
||||
next.add(index);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const selectAll = useCallback(() => {
|
||||
setSelectedIndices(new Set(results.questions.map((_, i) => i)));
|
||||
}, [results.questions]);
|
||||
|
||||
const deselectAll = useCallback(() => {
|
||||
setSelectedIndices(new Set());
|
||||
}, []);
|
||||
|
||||
// Reset all state
|
||||
const reset = useCallback(() => {
|
||||
setResults({ pack: null, questions: [], categoryIdeas: [], packIdeas: [] });
|
||||
setSelectedIndices(new Set());
|
||||
setConfig(prev => ({ ...prev, crudeLang: false, inspiration: '' }));
|
||||
setReviewState({
|
||||
reviews: [],
|
||||
summary: null,
|
||||
metadata: null,
|
||||
selectedGeneratorIndex: null,
|
||||
allCandidates: null,
|
||||
cherryPickResult: null,
|
||||
selectionMode: 'whole_set',
|
||||
});
|
||||
setFilter('all');
|
||||
setProgress(null);
|
||||
setSourceModel(null);
|
||||
setPreviousNames({ packs: [], categories: [], packIdeas: [] });
|
||||
}, []);
|
||||
|
||||
// Main generate function
|
||||
const generate = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setProgress(null);
|
||||
|
||||
// Reset review state
|
||||
setReviewState({
|
||||
reviews: [],
|
||||
summary: null,
|
||||
metadata: null,
|
||||
selectedGeneratorIndex: null,
|
||||
allCandidates: null,
|
||||
cherryPickResult: null,
|
||||
selectionMode: 'whole_set',
|
||||
});
|
||||
setFilter('all');
|
||||
setSourceModel(null);
|
||||
|
||||
const isExplicit = config.tone >= 4;
|
||||
|
||||
try {
|
||||
if (type === 'pack') {
|
||||
// Track previous names to avoid repetition
|
||||
const avoidNames = results.pack
|
||||
? [...previousNames.packs, results.pack.name]
|
||||
: previousNames.packs;
|
||||
if (results.pack) {
|
||||
setPreviousNames(prev => ({ ...prev, packs: avoidNames }));
|
||||
}
|
||||
|
||||
const model = getModel('generate');
|
||||
const result = await generatePack(
|
||||
context?.categoryName,
|
||||
isExplicit,
|
||||
config.crudeLang,
|
||||
config.inspiration || undefined,
|
||||
avoidNames
|
||||
);
|
||||
setResults(prev => ({ ...prev, pack: result }));
|
||||
setSourceModel(model);
|
||||
|
||||
} else if (type === 'questions') {
|
||||
if (!context?.packName) {
|
||||
toast.error('Pack name is required');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await generateQuestionsWithCouncil(
|
||||
context.packName,
|
||||
config.count,
|
||||
undefined,
|
||||
config.tone,
|
||||
context.packDescription || undefined,
|
||||
context.existingQuestions,
|
||||
config.crudeLang,
|
||||
config.inspiration || undefined,
|
||||
setProgress
|
||||
);
|
||||
|
||||
const questions = result.questions || [];
|
||||
const reviews = result.reviews || [];
|
||||
|
||||
setResults(prev => ({ ...prev, questions }));
|
||||
setReviewState({
|
||||
reviews,
|
||||
summary: result.summary,
|
||||
metadata: result.metadata,
|
||||
selectedGeneratorIndex: result.selectedGeneratorIndex,
|
||||
allCandidates: result.allCandidates,
|
||||
cherryPickResult: result.cherryPickResult || null,
|
||||
selectionMode: result.selectionMode || 'whole_set',
|
||||
});
|
||||
|
||||
// Smart selection based on review results
|
||||
if (reviews.length > 0) {
|
||||
const selected = new Set<number>();
|
||||
reviews.forEach(r => {
|
||||
if (r.verdict === 'pass' || r.verdict === 'flag') {
|
||||
selected.add(r.index);
|
||||
}
|
||||
});
|
||||
setSelectedIndices(selected);
|
||||
|
||||
if (result.summary) {
|
||||
const { passed, flagged, rejected } = result.summary;
|
||||
if (rejected > 0) {
|
||||
toast.warning(`Review complete: ${passed} passed, ${flagged} flagged, ${rejected} rejected`);
|
||||
} else {
|
||||
toast.success(`Review complete: ${passed} passed, ${flagged} flagged`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setSelectedIndices(new Set(questions.map((_, i) => i)));
|
||||
if (councilConfig.enabled && !result.metadata?.reviewerModel) {
|
||||
toast.warning('Council review failed - showing unreviewed questions');
|
||||
}
|
||||
}
|
||||
|
||||
} else if (type === 'category-ideas') {
|
||||
const avoidNames = results.categoryIdeas.length > 0
|
||||
? [...previousNames.categories, ...results.categoryIdeas.map(i => i.name)]
|
||||
: previousNames.categories;
|
||||
if (results.categoryIdeas.length > 0) {
|
||||
setPreviousNames(prev => ({ ...prev, categories: avoidNames }));
|
||||
}
|
||||
|
||||
const model = getModel('generate');
|
||||
const result = await suggestCategories(
|
||||
context?.existingCategories || [],
|
||||
isExplicit,
|
||||
config.crudeLang,
|
||||
config.inspiration || undefined,
|
||||
avoidNames
|
||||
);
|
||||
setResults(prev => ({ ...prev, categoryIdeas: result }));
|
||||
setSourceModel(model);
|
||||
|
||||
} else if (type === 'category-pack-ideas') {
|
||||
if (!context?.categoryName) {
|
||||
toast.error('Category name is required');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const avoidNames = results.packIdeas.length > 0
|
||||
? [...previousNames.packIdeas, ...results.packIdeas.map(i => i.name)]
|
||||
: previousNames.packIdeas;
|
||||
if (results.packIdeas.length > 0) {
|
||||
setPreviousNames(prev => ({ ...prev, packIdeas: avoidNames }));
|
||||
}
|
||||
|
||||
const model = getModel('generate');
|
||||
const result = await suggestPacks(
|
||||
context.categoryName,
|
||||
context?.existingPacks || [],
|
||||
isExplicit,
|
||||
config.crudeLang,
|
||||
config.inspiration || undefined,
|
||||
avoidNames
|
||||
);
|
||||
setResults(prev => ({ ...prev, packIdeas: result }));
|
||||
setSourceModel(model);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('AI generation error:', error);
|
||||
toast.error('Failed to generate content. Check your API key.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [type, context, config, results, previousNames, councilConfig.enabled]);
|
||||
|
||||
// Check if there are any results
|
||||
const hasResults = Boolean(
|
||||
results.pack ||
|
||||
results.questions.length > 0 ||
|
||||
results.categoryIdeas.length > 0 ||
|
||||
results.packIdeas.length > 0
|
||||
);
|
||||
|
||||
return {
|
||||
// Loading state
|
||||
loading,
|
||||
progress,
|
||||
|
||||
// Config
|
||||
config,
|
||||
setCount,
|
||||
setTone,
|
||||
setCrudeLang,
|
||||
setInspiration,
|
||||
|
||||
// Results
|
||||
results,
|
||||
hasResults,
|
||||
sourceModel,
|
||||
|
||||
// Review state
|
||||
reviewState,
|
||||
filter,
|
||||
setFilter,
|
||||
councilConfig,
|
||||
|
||||
// Selection
|
||||
selectedIndices,
|
||||
toggleSelection,
|
||||
selectAll,
|
||||
deselectAll,
|
||||
|
||||
// Helpers
|
||||
getReviewForIndex,
|
||||
getFilteredQuestionIndices,
|
||||
|
||||
// Actions
|
||||
generate,
|
||||
reset,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user