Skip to content

Commit c319caa

Browse files
amethystaniclaude
andcommitted
fix: correct api-keys table name, add error handling, and move AI generation into rescue_playbooks
- Fix api-keys edge function: was querying api_keys (wrong), now user_api_keys (correct) - KeysView: add isCreatingKey/createKeyError state so failures show error instead of blank screen - Remove duplicate playbooks sidebar tab and PlaybooksView (was added in error) - KnowledgeBase rescue_playbooks: add 'Generate with AI' button with date range picker that calls the /playbooks edge function and auto-populates templates from call history Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent acb51c9 commit c319caa

File tree

5 files changed

+137
-35
lines changed

5 files changed

+137
-35
lines changed

src/components/AppSidebar.tsx

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -343,17 +343,6 @@ export function AppSidebar({ activeTab, setActiveTab, hasAccess = false, ...prop
343343
<span>{t('sidebar.team')}</span>
344344
</SidebarMenuButton>
345345
</SidebarMenuItem>
346-
<SidebarMenuItem className={getLockedStyles('playbooks')}>
347-
<SidebarMenuButton
348-
isActive={activeTab === 'playbooks'}
349-
onClick={() => handleTabClick('playbooks')}
350-
tooltip={t('sidebar.playbooks') || 'AI Playbooks'}
351-
className={menuButtonClass}
352-
>
353-
<Wand2 />
354-
<span>{t('sidebar.playbooks') || 'Playbooks'}</span>
355-
</SidebarMenuButton>
356-
</SidebarMenuItem>
357346
<SidebarMenuItem className={getLockedStyles('integrations')}>
358347
<SidebarMenuButton
359348
isActive={activeTab === 'integrations'}

src/components/DashboardViews/KeysView.tsx

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,16 @@ export default function KeysView({ isDark = true, hasAccess = false }: { isDark?
3333
const [newKeyPermissions, setNewKeyPermissions] = useState<string[]>(['voice', 'text']);
3434
const [newKeyRateLimit, setNewKeyRateLimit] = useState(100);
3535
const [createdKeyToken, setCreatedKeyToken] = useState<string | null>(null);
36+
const [createKeyError, setCreateKeyError] = useState<string | null>(null);
37+
const [isCreatingKey, setIsCreatingKey] = useState(false);
3638

3739
const resetForm = () => {
3840
setNewKeyName('');
3941
setNewKeyPermissions(['voice', 'text']);
4042
setNewKeyRateLimit(100);
4143
setWizardStep('name');
4244
setCreatedKeyToken(null);
45+
setCreateKeyError(null);
4346
};
4447

4548
const openCreateModal = () => {
@@ -97,6 +100,8 @@ export default function KeysView({ isDark = true, hasAccess = false }: { isDark?
97100
const createKey = async () => {
98101
if (!user?.id) return;
99102

103+
setIsCreatingKey(true);
104+
setCreateKeyError(null);
100105
try {
101106
const headers = await getAuthHeaders();
102107
const res = await fetch(apiBase, {
@@ -126,10 +131,12 @@ export default function KeysView({ isDark = true, hasAccess = false }: { isDark?
126131
setCreatedKeyToken(data.token);
127132
setWizardStep('created');
128133
} else {
129-
console.error('Failed to create API key:', result.error);
134+
setCreateKeyError(result.error || 'Failed to create API key.');
130135
}
131136
} catch (err) {
132-
console.error('Failed to create API key:', err);
137+
setCreateKeyError('Network error. Please try again.');
138+
} finally {
139+
setIsCreatingKey(false);
133140
}
134141
};
135142

@@ -651,6 +658,12 @@ export default function KeysView({ isDark = true, hasAccess = false }: { isDark?
651658
<p className={cn("text-sm", isDark ? "text-white/60" : "text-gray-600")}>
652659
{t('keys.modal.confirmDesc')}
653660
</p>
661+
{createKeyError && (
662+
<div className={cn("p-3 rounded-lg flex items-start gap-3", isDark ? "bg-rose-500/10" : "bg-rose-50")}>
663+
<AlertTriangle className={cn("w-4 h-4 mt-0.5 flex-shrink-0", isDark ? "text-rose-400" : "text-rose-500")} />
664+
<p className={cn("text-xs", isDark ? "text-rose-400" : "text-rose-600")}>{createKeyError}</p>
665+
</div>
666+
)}
654667

655668
<div className={cn(
656669
"rounded-lg border divide-y",
@@ -765,16 +778,16 @@ export default function KeysView({ isDark = true, hasAccess = false }: { isDark?
765778
)}
766779
<button
767780
onClick={nextStep}
768-
disabled={!canProceed()}
781+
disabled={!canProceed() || isCreatingKey}
769782
className={cn(
770783
"flex-1 py-3 rounded-lg text-sm font-medium transition-colors flex items-center justify-center gap-2",
771-
canProceed()
784+
canProceed() && !isCreatingKey
772785
? (isDark ? "bg-white text-black hover:bg-white/90" : "bg-black text-white hover:bg-black/90")
773786
: (isDark ? "bg-white/10 text-white/30 cursor-not-allowed" : "bg-gray-100 text-gray-400 cursor-not-allowed")
774787
)}
775788
>
776-
{wizardStep === 'confirm' ? t('keys.createKey') : t('keys.modal.continue')}
777-
<ArrowRight className="w-4 h-4" />
789+
{isCreatingKey ? <Loader2 className="w-4 h-4 animate-spin" /> : wizardStep === 'confirm' ? t('keys.createKey') : t('keys.modal.continue')}
790+
{!isCreatingKey && <ArrowRight className="w-4 h-4" />}
778791
</button>
779792
</>
780793
)}

src/components/DemoCall/KnowledgeBase.tsx

Lines changed: 113 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import { localLLMService } from '../../services/localLLMService';
2424
import { DEFAULT_VOICE_ID, normalizeVoiceId } from '../../config/voiceConfig';
2525
import { useRescueCenter } from '../../contexts/RescueCenterContext';
2626
import type { RescueChannel, RescuePlaybookTemplate } from '../../types/rescuePlaybook';
27+
import { supabase } from '../../config/supabase';
28+
import { Wand2 } from 'lucide-react';
2729

2830
// Define the tabs structure
2931
type 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 }}>

src/pages/DemoDashboard.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import ActivityLogView from '../components/DashboardViews/ActivityLogView';
2727
import WebhooksView from '../components/DashboardViews/WebhooksView';
2828
import NotificationCenter from '../components/DashboardViews/NotificationCenter';
2929
import InitialSetupDialog from '../components/DashboardViews/InitialSetupDialog';
30-
import PlaybooksView from '../components/DashboardViews/PlaybooksView';
3130
import { RescueCenterProvider } from '../contexts/RescueCenterContext';
3231

3332
import { AccessCodeProvider, useAccessCode } from '../contexts/AccessCodeContext';
@@ -84,8 +83,6 @@ function DemoDashboardContent() {
8483
customer_graph: t('sidebar.customerGraph'),
8584
system_prompt: t('sidebar.systemPrompt'),
8685
rescue_playbooks: t('sidebar.rescuePlaybooks'),
87-
playbooks: t('sidebar.playbooks') || 'AI Playbooks',
88-
8986
context_fields: t('sidebar.contextFields'),
9087
categories: t('sidebar.categories'),
9188
priority_rules: t('sidebar.priorityRules'),
@@ -219,8 +216,7 @@ function DemoDashboardContent() {
219216
{activeTab === 'billing' && <BillingView isDark={isDark} hasAccess={hasAccess} />}
220217
{activeTab === 'keys' && <KeysView isDark={isDark} hasAccess={hasAccess} />}
221218
{activeTab === 'team' && <TeamView isDark={isDark} />}
222-
{activeTab === 'playbooks' && <PlaybooksView isDark={isDark} />}
223-
{activeTab === 'integrations' && <IntegrationView isDark={isDark} />}
219+
{activeTab === 'integrations' && <IntegrationView isDark={isDark} />}
224220
{activeTab === 'activity_log' && <ActivityLogView isDark={isDark} />}
225221
{activeTab === 'webhooks' && <WebhooksView isDark={isDark} />}
226222
{activeTab === 'settings' && <SettingsView isDark={isDark} />}

supabase/functions/api-keys/index.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ Deno.serve(async (req: Request) => {
108108
// ─── GET: List API keys ──────────────────────────────────
109109
if (req.method === 'GET') {
110110
const { data, error } = await adminClient
111-
.from('api_keys')
111+
.from('user_api_keys')
112112
.select('*')
113113
.eq('user_id', user.id)
114114
.order('created_at', { ascending: false });
@@ -137,7 +137,7 @@ Deno.serve(async (req: Request) => {
137137

138138
// Check key count limit (max 10 per user)
139139
const { count } = await adminClient
140-
.from('api_keys')
140+
.from('user_api_keys')
141141
.select('id', { count: 'exact', head: true })
142142
.eq('user_id', user.id)
143143
.eq('status', 'active');
@@ -149,7 +149,7 @@ Deno.serve(async (req: Request) => {
149149
const token = generateApiToken();
150150

151151
const { data, error } = await adminClient
152-
.from('api_keys')
152+
.from('user_api_keys')
153153
.insert({
154154
user_id: user.id,
155155
name,
@@ -175,7 +175,7 @@ Deno.serve(async (req: Request) => {
175175
}
176176

177177
const { error } = await adminClient
178-
.from('api_keys')
178+
.from('user_api_keys')
179179
.delete()
180180
.eq('id', keyId)
181181
.eq('user_id', user.id);

0 commit comments

Comments
 (0)