@@ -24,6 +24,8 @@ import { localLLMService } from '../../services/localLLMService';
2424import { DEFAULT_VOICE_ID , normalizeVoiceId } from '../../config/voiceConfig' ;
2525import { useRescueCenter } from '../../contexts/RescueCenterContext' ;
2626import type { RescueChannel , RescuePlaybookTemplate } from '../../types/rescuePlaybook' ;
27+ import { supabase } from '../../config/supabase' ;
28+ import { Wand2 } from 'lucide-react' ;
2729
2830// Define the tabs structure
2931type ActiveTab = 'prompt' | 'voice' | 'fields' | 'categories' | 'rules' | 'instructions' | 'rescue_playbooks' ;
@@ -99,6 +101,56 @@ export default function KnowledgeBase({ isDark = true, activeSection }: Knowledg
99101 const [ isAddingInstruction , setIsAddingInstruction ] = useState ( false ) ;
100102 const [ newInstruction , setNewInstruction ] = useState ( '' ) ;
101103 const [ isAddingPlaybook , setIsAddingPlaybook ] = useState ( false ) ;
104+ const [ showAIGenerate , setShowAIGenerate ] = useState ( false ) ;
105+ const [ aiGenStartDate , setAiGenStartDate ] = useState ( ( ) => {
106+ const d = new Date ( ) ;
107+ d . setMonth ( d . getMonth ( ) - 1 ) ;
108+ return d . toISOString ( ) . split ( 'T' ) [ 0 ] ;
109+ } ) ;
110+ const [ aiGenEndDate , setAiGenEndDate ] = useState ( ( ) => new Date ( ) . toISOString ( ) . split ( 'T' ) [ 0 ] ) ;
111+ const [ isGeneratingAI , setIsGeneratingAI ] = useState ( false ) ;
112+ const [ aiGenError , setAiGenError ] = useState < string | null > ( null ) ;
113+
114+ const generatePlaybooksWithAI = async ( ) => {
115+ setIsGeneratingAI ( true ) ;
116+ setAiGenError ( null ) ;
117+ try {
118+ const { data : { session } } = await supabase . auth . getSession ( ) ;
119+ if ( ! session ) { setAiGenError ( 'Not authenticated' ) ; return ; }
120+
121+ const res = await fetch ( `${ import . meta. env . VITE_SUPABASE_URL } /functions/v1/playbooks` , {
122+ method : 'POST' ,
123+ headers : {
124+ 'Authorization' : `Bearer ${ session . access_token } ` ,
125+ 'Content-Type' : 'application/json' ,
126+ } ,
127+ body : JSON . stringify ( { start_date : aiGenStartDate , end_date : aiGenEndDate } ) ,
128+ } ) ;
129+ const result = await res . json ( ) ;
130+ if ( ! res . ok ) { setAiGenError ( result . error || 'Failed to generate playbooks' ) ; return ; }
131+
132+ const newTemplates : RescuePlaybookTemplate [ ] = ( result . templates || [ ] ) . map ( ( t : any ) => ( {
133+ id : t . id || `ai-${ Date . now ( ) } -${ Math . random ( ) } ` ,
134+ name : t . name ,
135+ description : t . description || '' ,
136+ channels : [ 'email' ] as RescueChannel [ ] ,
137+ messageTemplate : ( t . recommended_actions || [ ] ) . join ( '\n• ' ) ,
138+ voiceScript : ( t . recommended_actions || [ ] ) . join ( '. ' ) ,
139+ creditAmountInr : 0 ,
140+ discountPercent : 0 ,
141+ successCriteria : ( t . success_indicators || [ ] ) . join ( ', ' ) ,
142+ enabled : true ,
143+ } ) ) ;
144+
145+ setPlaybooks ( [ ...playbooks , ...newTemplates ] ) ;
146+ setShowAIGenerate ( false ) ;
147+ } catch {
148+ setAiGenError ( 'Network error. Please try again.' ) ;
149+ } finally {
150+ setIsGeneratingAI ( false ) ;
151+ }
152+ } ;
153+
102154 const [ newPlaybook , setNewPlaybook ] = useState ( {
103155 name : '' ,
104156 description : '' ,
@@ -726,17 +778,69 @@ export default function KnowledgeBase({ isDark = true, activeSection }: Knowledg
726778
727779 < div className = { cn ( "flex justify-between items-center pb-2 border-b" , isDark ? "border-white/10" : "border-gray-100" ) } >
728780 < h3 className = { cn ( "text-lg font-semibold" , isDark ? "text-white" : "text-gray-900" ) } > Action Templates</ h3 >
729- < button
730- onClick = { ( ) => setIsAddingPlaybook ( true ) }
731- className = { cn (
732- "px-3 py-1.5 rounded-lg text-sm font-medium border transition-colors" ,
733- isDark ? "bg-white text-black hover:bg-white/90" : "bg-black text-white hover:bg-black/90"
734- ) }
735- >
736- Add Template
737- </ button >
781+ < div className = "flex gap-2" >
782+ < button
783+ onClick = { ( ) => { setShowAIGenerate ( ! showAIGenerate ) ; setIsAddingPlaybook ( false ) ; } }
784+ className = { cn (
785+ "px-3 py-1.5 rounded-lg text-sm font-medium border transition-colors flex items-center gap-1.5" ,
786+ isDark ? "border-white/10 text-white hover:bg-white/10" : "border-black/10 text-black hover:bg-black/5"
787+ ) }
788+ >
789+ < Wand2 className = "w-3.5 h-3.5" />
790+ Generate with AI
791+ </ button >
792+ < button
793+ onClick = { ( ) => { setIsAddingPlaybook ( true ) ; setShowAIGenerate ( false ) ; } }
794+ className = { cn (
795+ "px-3 py-1.5 rounded-lg text-sm font-medium border transition-colors" ,
796+ isDark ? "bg-white text-black hover:bg-white/90" : "bg-black text-white hover:bg-black/90"
797+ ) }
798+ >
799+ Add Template
800+ </ button >
801+ </ div >
738802 </ div >
739803
804+ < AnimatePresence >
805+ { showAIGenerate && (
806+ < motion . div initial = { { opacity : 0 , y : - 8 } } animate = { { opacity : 1 , y : 0 } } exit = { { opacity : 0 , y : - 8 } } >
807+ < div className = { cn ( "rounded-xl border p-4 space-y-3" , isDark ? "border-white/10 bg-white/[0.03]" : "border-black/10 bg-black/[0.02]" ) } >
808+ < p className = { cn ( "text-xs" , isDark ? "text-white/50" : "text-black/50" ) } >
809+ Analyze your call history to auto-generate action templates using AI.
810+ </ p >
811+ < div className = "grid grid-cols-2 gap-3" >
812+ < label className = "space-y-1" >
813+ < span className = { cn ( "text-[11px] uppercase tracking-wide" , isDark ? "text-white/45" : "text-black/45" ) } > From</ span >
814+ < input type = "date" value = { aiGenStartDate } onChange = { e => setAiGenStartDate ( e . target . value ) }
815+ className = { cn ( "w-full rounded-lg px-3 py-2 text-sm border bg-transparent focus:outline-none" , isDark ? "border-white/10 text-white" : "border-black/10 text-black" ) } />
816+ </ label >
817+ < label className = "space-y-1" >
818+ < span className = { cn ( "text-[11px] uppercase tracking-wide" , isDark ? "text-white/45" : "text-black/45" ) } > To</ span >
819+ < input type = "date" value = { aiGenEndDate } onChange = { e => setAiGenEndDate ( e . target . value ) }
820+ className = { cn ( "w-full rounded-lg px-3 py-2 text-sm border bg-transparent focus:outline-none" , isDark ? "border-white/10 text-white" : "border-black/10 text-black" ) } />
821+ </ label >
822+ </ div >
823+ { aiGenError && (
824+ < div className = { cn ( "flex items-center gap-2 text-xs p-2 rounded-lg" , isDark ? "bg-rose-500/10 text-rose-400" : "bg-rose-50 text-rose-600" ) } >
825+ < AlertTriangle className = "w-3.5 h-3.5 flex-shrink-0" />
826+ { aiGenError }
827+ </ div >
828+ ) }
829+ < div className = "flex justify-end gap-2" >
830+ < button onClick = { ( ) => { setShowAIGenerate ( false ) ; setAiGenError ( null ) ; } }
831+ className = { cn ( "text-xs px-3 py-1.5 rounded" , isDark ? "text-white/60 hover:bg-white/10" : "text-black/60 hover:bg-black/10" ) } >
832+ Cancel
833+ </ button >
834+ < button onClick = { generatePlaybooksWithAI } disabled = { isGeneratingAI }
835+ className = { cn ( "text-xs font-semibold px-3 py-1.5 rounded flex items-center gap-1.5" , isGeneratingAI ? "bg-white/20 text-white/50 cursor-not-allowed" : "bg-white text-black" ) } >
836+ { isGeneratingAI ? < > < Loader2 className = "w-3 h-3 animate-spin" /> Analyzing...</ > : < > < Wand2 className = "w-3 h-3" /> Generate</ > }
837+ </ button >
838+ </ div >
839+ </ div >
840+ </ motion . div >
841+ ) }
842+ </ AnimatePresence >
843+
740844 < AnimatePresence >
741845 { isAddingPlaybook && (
742846 < motion . div initial = { { opacity : 0 , y : - 8 } } animate = { { opacity : 1 , y : 0 } } exit = { { opacity : 0 , y : - 8 } } >
0 commit comments