11import { NextRequest , NextResponse } from "next/server" ;
22import { validateAuthOrReject , isAuthError } from "@/lib/auth" ;
33import { createSupabaseAdminClient } from "@/app/utils/supabase/supabaseAdmin" ;
4+ import { GoogleGenerativeAI } from "@google/generative-ai" ;
45
56/**
67 * POST /api/cre/trigger
78 * Triggers CRE story verification workflow.
8- * Sends story content directly in the CRE trigger payload (Approach B).
9+ *
10+ * When CRE_WORKFLOW_URL is set: triggers the deployed CRE workflow (production).
11+ * When CRE_WORKFLOW_URL is NOT set: runs Gemini analysis directly and writes
12+ * metrics to Supabase (fallback for demo/dev when CRE isn't deployed yet).
913 */
1014export async function POST ( req : NextRequest ) {
1115 try {
@@ -83,9 +87,10 @@ export async function POST(req: NextRequest) {
8387 return NextResponse . json ( { error : "Failed to start verification" } , { status : 500 } ) ;
8488 }
8589
86- // Trigger CRE workflow with content in payload (Approach B — standard pattern )
90+ // Route: CRE workflow (production) or direct Gemini fallback (demo/dev )
8791 const creWorkflowUrl = process . env . CRE_WORKFLOW_URL ;
8892 if ( creWorkflowUrl ) {
93+ // Production path: trigger deployed CRE workflow
8994 fetch ( creWorkflowUrl , {
9095 method : "POST" ,
9196 headers : {
@@ -100,7 +105,11 @@ export async function POST(req: NextRequest) {
100105 } ) ,
101106 } ) . catch ( err => console . error ( "[CRE/TRIGGER] Workflow trigger failed:" , err ) ) ;
102107 } else {
103- console . warn ( "[CRE/TRIGGER] CRE_WORKFLOW_URL not set, skipping workflow trigger" ) ;
108+ // Fallback: direct Gemini analysis (same AI, no on-chain attestation)
109+ console . log ( "[CRE/TRIGGER] No CRE_WORKFLOW_URL — using direct Gemini fallback" ) ;
110+ runDirectGeminiFallback ( admin , story . id , story . title || "Untitled" , story . content ) . catch (
111+ err => console . error ( "[CRE/TRIGGER] Direct fallback failed:" , err )
112+ ) ;
104113 }
105114
106115 return NextResponse . json ( {
@@ -113,3 +122,129 @@ export async function POST(req: NextRequest) {
113122 return NextResponse . json ( { error : "Internal server error" } , { status : 500 } ) ;
114123 }
115124}
125+
126+ // ============================================================================
127+ // Direct Gemini Fallback (used when CRE_WORKFLOW_URL is not set)
128+ // ============================================================================
129+
130+ const VERIFICATION_PROMPT = `You are a content quality analyst. Analyze the provided story and return a JSON object with exactly these fields:
131+
132+ - significanceScore: number 0-100 (how meaningful/impactful to the author's personal growth)
133+ - emotionalDepth: number 1-5 (1=surface, 2=mild, 3=moderate, 4=deep, 5=profound)
134+ - qualityScore: number 0-100 (writing quality: coherence, structure, vocabulary, narrative flow)
135+ - wordCount: number (exact word count of the content)
136+ - themes: string[] (2-5 main themes, lowercase, e.g. ["growth", "family", "resilience"])
137+
138+ STRICT RULES:
139+ - Output MUST be valid JSON. No markdown, no backticks, no explanation.
140+ - Return ONLY the JSON object.` ;
141+
142+ interface DirectMetrics {
143+ significanceScore : number ;
144+ emotionalDepth : number ;
145+ qualityScore : number ;
146+ wordCount : number ;
147+ themes : string [ ] ;
148+ }
149+
150+ function clamp ( value : number , min : number , max : number ) : number {
151+ return Math . min ( max , Math . max ( min , value ) ) ;
152+ }
153+
154+ function scoreToTier ( score : number ) : number {
155+ if ( score <= 20 ) return 1 ;
156+ if ( score <= 40 ) return 2 ;
157+ if ( score <= 60 ) return 3 ;
158+ if ( score <= 80 ) return 4 ;
159+ return 5 ;
160+ }
161+
162+ /**
163+ * Runs the same Gemini analysis as the CRE workflow but directly,
164+ * then writes results to verified_metrics in Supabase.
165+ * No on-chain attestation — source marked as "direct".
166+ */
167+ async function runDirectGeminiFallback (
168+ admin : ReturnType < typeof createSupabaseAdminClient > ,
169+ storyId : string ,
170+ title : string ,
171+ content : string
172+ ) {
173+ const apiKey = process . env . GOOGLE_GENERATIVE_AI_API_KEY ;
174+ if ( ! apiKey ) {
175+ console . error ( "[CRE/DIRECT] GOOGLE_GENERATIVE_AI_API_KEY not set" ) ;
176+ return ;
177+ }
178+
179+ const genAI = new GoogleGenerativeAI ( apiKey ) ;
180+ const model = genAI . getGenerativeModel ( {
181+ model : "gemini-2.0-flash" ,
182+ generationConfig : { temperature : 0.1 , responseMimeType : "application/json" } ,
183+ } ) ;
184+
185+ const prompt = `${ VERIFICATION_PROMPT } \n\nTitle: "${ title } "\n\nContent:\n"""\n${ content } \n"""` ;
186+
187+ const result = await model . generateContent ( prompt ) ;
188+ const text = result . response . text ( ) ;
189+
190+ // Parse response
191+ const cleaned = text . replace ( / ` ` ` j s o n \s * / gi, "" ) . replace ( / ` ` ` \s * / gi, "" ) . trim ( ) ;
192+ const jsonMatch = cleaned . match ( / \{ [ \s \S ] * \} / ) ;
193+ if ( ! jsonMatch ) {
194+ console . error ( "[CRE/DIRECT] No JSON found in Gemini response" ) ;
195+ return ;
196+ }
197+
198+ const raw = JSON . parse ( jsonMatch [ 0 ] ) as Partial < DirectMetrics > ;
199+
200+ const metrics : DirectMetrics = {
201+ significanceScore : clamp ( Math . round ( Number ( raw . significanceScore ) || 0 ) , 0 , 100 ) ,
202+ emotionalDepth : clamp ( Math . round ( Number ( raw . emotionalDepth ) || 1 ) , 1 , 5 ) ,
203+ qualityScore : clamp ( Math . round ( Number ( raw . qualityScore ) || 0 ) , 0 , 100 ) ,
204+ wordCount : Math . max ( 0 , Math . round ( Number ( raw . wordCount ) || 0 ) ) ,
205+ themes : Array . isArray ( raw . themes )
206+ ? raw . themes . filter ( ( t ) : t is string => typeof t === "string" ) . map ( t => t . toLowerCase ( ) . trim ( ) ) . slice ( 0 , 5 )
207+ : [ ] ,
208+ } ;
209+
210+ const qualityTier = scoreToTier ( metrics . qualityScore ) ;
211+
212+ console . log (
213+ `[CRE/DIRECT] Analysis complete: significance=${ metrics . significanceScore } , quality=${ metrics . qualityScore } , tier=${ qualityTier } `
214+ ) ;
215+
216+ // Write to verified_metrics — same schema as CRE callback
217+ const { error : upsertError } = await admin
218+ . from ( "verified_metrics" )
219+ . upsert (
220+ {
221+ story_id : storyId ,
222+ significance_score : metrics . significanceScore ,
223+ emotional_depth : metrics . emotionalDepth ,
224+ quality_score : metrics . qualityScore ,
225+ word_count : metrics . wordCount ,
226+ verified_themes : metrics . themes ,
227+ quality_tier : qualityTier ,
228+ meets_quality_threshold : metrics . qualityScore >= 70 ,
229+ metrics_hash : null , // No CRE hash — direct analysis
230+ on_chain_tx_hash : null , // No on-chain write — direct analysis
231+ cre_attestation_id : null ,
232+ updated_at : new Date ( ) . toISOString ( ) ,
233+ } ,
234+ { onConflict : "story_id" }
235+ ) ;
236+
237+ if ( upsertError ) {
238+ console . error ( "[CRE/DIRECT] Supabase upsert error:" , upsertError ) ;
239+ return ;
240+ }
241+
242+ // Mark verification as completed
243+ await admin
244+ . from ( "verification_logs" )
245+ . update ( { status : "completed" , updated_at : new Date ( ) . toISOString ( ) } )
246+ . eq ( "story_id" , storyId )
247+ . eq ( "status" , "pending" ) ;
248+
249+ console . log ( `[CRE/DIRECT] Metrics saved for story ${ storyId } ` ) ;
250+ }
0 commit comments