+2
-2
.cursor/rules.json
+2
-2
.cursor/rules.json
+9
-3
app/api/debug/categorize-pr/route.ts
+9
-3
app/api/debug/categorize-pr/route.ts
···
57
57
58
58
console.log(`DEBUG: Starting categorization for PR #${pullRequest.number} (ID: ${prId}) in ${repository.full_name}`);
59
59
60
-
// Get AI settings for organization
60
+
// Get API settings for organization
61
61
const aiSettings = await getOrganizationAiSettings(organizationId);
62
62
if (!aiSettings.selectedModelId || aiSettings.selectedModelId === '__none__') {
63
63
return NextResponse.json({
···
66
66
}
67
67
68
68
// Get provider and model info
69
+
const provider = aiSettings.provider;
69
70
const selectedModelId = aiSettings.selectedModelId;
70
-
const [provider] = selectedModelId.split('/');
71
+
72
+
if (!provider) {
73
+
return NextResponse.json({
74
+
error: 'AI provider not set for organization'
75
+
}, { status: 400 });
76
+
}
71
77
72
78
// Get API key for provider
73
-
const apiKey = await getOrganizationApiKey(organizationId, provider as 'openai' | 'google' | 'anthropic');
79
+
const apiKey = await getOrganizationApiKey(organizationId, provider);
74
80
75
81
if (!apiKey) {
76
82
return NextResponse.json({
+14
-16
app/api/webhook/github/route.ts
+14
-16
app/api/webhook/github/route.ts
···
673
673
}
674
674
}
675
675
676
-
// 1. Fetch Organization's AI Settings (using organizationId)
676
+
// 1. Get API settings (model + API key)
677
+
console.log(`WEBHOOK AI SETTINGS: Getting AI settings for organization ${organizationId}`);
677
678
const aiSettings = await getOrganizationAiSettings(organizationId);
678
-
console.log(`WEBHOOK AI SETTINGS: Organization ${organizationId} AI settings:`,
679
-
aiSettings ? `model=${aiSettings.selectedModelId}` : "No settings found");
680
-
681
-
if (!aiSettings || !aiSettings.selectedModelId) {
679
+
const selectedModelId = aiSettings.selectedModelId;
680
+
681
+
if (!selectedModelId || selectedModelId === '__none__') {
682
682
console.log(`WEBHOOK INFO: AI categorization disabled for organization ${organizationId} (no model selected).`);
683
683
await PullRequestRepository.updatePullRequest(prDbId, { ai_status: 'skipped', error_message: 'AI categorization disabled for organization' });
684
684
return;
685
685
}
686
-
687
-
const selectedModelId = aiSettings.selectedModelId;
688
-
console.log(`WEBHOOK AI MODEL: Organization ${organizationId} selected AI model: ${selectedModelId}`);
689
-
690
-
const modelInfo = allModels.find(m => m.id === selectedModelId);
691
-
if (!modelInfo) {
692
-
console.error(`WEBHOOK ERROR: Selected model ID ${selectedModelId} not found in allModels for organization ${organizationId}.`);
693
-
await PullRequestRepository.updatePullRequest(prDbId, { ai_status: 'error', error_message: `Selected AI model ${selectedModelId} not configured` });
686
+
687
+
const provider = aiSettings.provider;
688
+
689
+
if (!provider) {
690
+
console.log(`WEBHOOK INFO: AI provider not set for organization ${organizationId}.`);
691
+
await PullRequestRepository.updatePullRequest(prDbId, { ai_status: 'skipped', error_message: 'AI provider not set for organization' });
694
692
return;
695
693
}
696
-
const provider = modelInfo.provider;
697
-
console.log(`WEBHOOK AI PROVIDER: Determined provider: ${provider} for model ${selectedModelId}`);
698
-
694
+
695
+
console.log(`WEBHOOK MODEL: Using AI model ${selectedModelId} with provider ${provider}`);
696
+
699
697
const apiKey = await getOrganizationApiKey(organizationId, provider);
700
698
if (!apiKey) {
701
699
console.warn(`WEBHOOK WARNING: API key for provider ${provider} not set for organization ${organizationId}. Skipping AI categorization.`);
+2
-3
app/dashboard/analytics/page.tsx
+2
-3
app/dashboard/analytics/page.tsx
···
21
21
>
22
22
<AppSidebar variant="inset" />
23
23
<SidebarInset>
24
-
<SiteHeader />
24
+
<SiteHeader pageTitle="Team Collaboration Insights" />
25
25
<div className="flex flex-1 flex-col">
26
26
<div className="@container/main flex flex-1 flex-col gap-2">
27
27
<div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6">
28
-
<div className="flex justify-between items-center px-4 lg:px-6">
29
-
<h1 className="text-2xl font-semibold">Team Collaboration Insights</h1>
28
+
<div className="flex justify-end items-center px-4 lg:px-6">
30
29
<Button variant="outline" size="sm">
31
30
<IconDownload className="mr-2 h-4 w-4" />
32
31
Export Team Report
+1
-4
app/dashboard/debug/page.tsx
+1
-4
app/dashboard/debug/page.tsx
···
70
70
>
71
71
<AppSidebar variant="inset" />
72
72
<SidebarInset>
73
-
<SiteHeader />
73
+
<SiteHeader pageTitle="Debug Information" />
74
74
<div className="flex flex-1 flex-col">
75
75
<div className="@container/main flex flex-1 flex-col gap-2">
76
76
<div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6">
77
-
<div className="px-4 lg:px-6">
78
-
<h1 className="text-2xl font-semibold">Debug Information</h1>
79
-
</div>
80
77
<div className="grid gap-4 px-4 lg:px-6">
81
78
<Card>
82
79
<CardHeader>
+1
-2
app/dashboard/lifecycle/page.tsx
+1
-2
app/dashboard/lifecycle/page.tsx
···
21
21
>
22
22
<AppSidebar variant="inset" />
23
23
<SidebarInset>
24
-
<SiteHeader />
24
+
<SiteHeader pageTitle="PR Lifecycle" />
25
25
<div className="flex flex-1 flex-col">
26
26
<div className="@container/main flex flex-1 flex-col gap-2">
27
27
<div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6">
28
28
<div className="flex justify-between items-center px-4 lg:px-6">
29
-
<h1 className="text-2xl font-semibold">PR Lifecycle</h1>
30
29
<div className="flex gap-3">
31
30
<RepositoryFilter />
32
31
<DateRangePickerWithPresets />
+1
-4
app/dashboard/page.tsx
+1
-4
app/dashboard/page.tsx
···
216
216
>
217
217
<AppSidebar variant="inset" />
218
218
<SidebarInset>
219
-
<SiteHeader />
219
+
<SiteHeader pageTitle="Dashboard Overview" />
220
220
{setupIncomplete && (
221
221
<div className="px-4 pt-4 lg:px-6">
222
222
<SetupStatusAlert />
···
225
225
<div className="flex flex-1 flex-col">
226
226
<div className="@container/main flex flex-1 flex-col gap-2">
227
227
<div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6">
228
-
<div className="px-4 lg:px-6">
229
-
<h1 className="text-2xl font-semibold">Dashboard Overview</h1>
230
-
</div>
231
228
<SectionCardsEngineering />
232
229
<div className="px-4 lg:px-6">
233
230
<ActionableRecommendations />
+1
-4
app/dashboard/projects/page.tsx
+1
-4
app/dashboard/projects/page.tsx
···
18
18
>
19
19
<AppSidebar variant="inset" />
20
20
<SidebarInset>
21
-
<SiteHeader />
21
+
<SiteHeader pageTitle="Projects & Repositories" />
22
22
<div className="flex flex-1 flex-col">
23
23
<div className="@container/main flex flex-1 flex-col gap-2">
24
24
<div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6">
25
-
<div className="px-4 lg:px-6">
26
-
<h1 className="text-2xl font-semibold">Projects & Repositories</h1>
27
-
</div>
28
25
<RepositoryInsights />
29
26
</div>
30
27
</div>
+1
-5
app/dashboard/settings/page.tsx
+1
-5
app/dashboard/settings/page.tsx
···
38
38
>
39
39
<AppSidebar variant="inset" />
40
40
<SidebarInset>
41
-
<SiteHeader />
41
+
<SiteHeader pageTitle="Settings" />
42
42
<div className="flex flex-1 flex-col">
43
43
<div className="@container/main flex flex-1 flex-col gap-2">
44
44
<div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6">
45
-
<div className="px-4 lg:px-6">
46
-
<h1 className="text-2xl font-semibold">Settings</h1>
47
-
</div>
48
-
49
45
<Tabs defaultValue="github" className="px-4 lg:px-6">
50
46
<TabsList>
51
47
<TabsTrigger value="github">GitHub</TabsTrigger>
+1
-4
app/dashboard/team/page.tsx
+1
-4
app/dashboard/team/page.tsx
···
18
18
>
19
19
<AppSidebar variant="inset" />
20
20
<SidebarInset>
21
-
<SiteHeader />
21
+
<SiteHeader pageTitle="Team Performance" />
22
22
<div className="flex flex-1 flex-col">
23
23
<div className="@container/main flex flex-1 flex-col gap-2">
24
24
<div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6">
25
-
<div className="px-4 lg:px-6">
26
-
<h1 className="text-2xl font-semibold">Team Performance</h1>
27
-
</div>
28
25
<DeveloperPerformance />
29
26
</div>
30
27
</div>
+378
-25
components/onboarding-wizard.tsx
+378
-25
components/onboarding-wizard.tsx
···
6
6
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
7
7
import { Progress } from '@/components/ui/progress';
8
8
import { InstallGitHubApp } from '@/components/ui/install-github-app';
9
-
import { CheckCircle2, ChevronRight, Github } from 'lucide-react';
9
+
import { CheckCircle2, ChevronRight, Github, CheckIcon } from 'lucide-react';
10
10
import { toast } from 'sonner';
11
11
import { useRouter } from 'next/navigation';
12
+
import { allModels, ModelDefinition } from '@/lib/ai-models';
13
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
14
+
import { Input } from '@/components/ui/input';
15
+
import { Label } from '@/components/ui/label';
16
+
import { Organization, Repository } from '@/lib/types';
17
+
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
18
+
import { Checkbox } from '@/components/ui/checkbox';
12
19
13
20
// Define the onboarding steps
14
21
const STEPS = {
···
19
26
COMPLETE: 4
20
27
};
21
28
29
+
// Define minimal organization and repository types
30
+
interface GitHubOrganization {
31
+
id: number;
32
+
login: string;
33
+
avatar_url?: string;
34
+
}
35
+
36
+
interface GitHubRepository {
37
+
id: number;
38
+
name: string;
39
+
full_name: string;
40
+
private: boolean;
41
+
}
42
+
22
43
export function OnboardingWizard() {
23
44
const { data: session, status } = useSession();
24
45
const router = useRouter();
25
46
const [currentStep, setCurrentStep] = useState(STEPS.WELCOME);
26
47
const [githubAppInstalled, setGithubAppInstalled] = useState(false);
27
48
49
+
// Add AI settings state
50
+
const [selectedProvider, setSelectedProvider] = useState<'openai' | 'google' | 'anthropic' | null>(null);
51
+
const [selectedModelId, setSelectedModelId] = useState<string | null>(null);
52
+
const [apiKey, setApiKey] = useState<string>('');
53
+
54
+
// Add organization and repository state
55
+
const [organizations, setOrganizations] = useState<GitHubOrganization[]>([]);
56
+
const [selectedOrg, setSelectedOrg] = useState<GitHubOrganization | null>(null);
57
+
const [repositories, setRepositories] = useState<GitHubRepository[]>([]);
58
+
const [selectedRepoIds, setSelectedRepoIds] = useState<Set<number>>(new Set());
59
+
const [isLoadingOrgs, setIsLoadingOrgs] = useState(false);
60
+
const [isLoadingRepos, setIsLoadingRepos] = useState(false);
61
+
62
+
// Filter models based on selected provider
63
+
const availableModels = selectedProvider
64
+
? allModels.filter(model => model.provider === selectedProvider)
65
+
: [];
66
+
67
+
// Load GitHub data when on repository selection step
68
+
useEffect(() => {
69
+
const fetchGitHubData = async () => {
70
+
if (currentStep === STEPS.REPO_SELECT) {
71
+
await fetchGitHubOrganizations();
72
+
}
73
+
};
74
+
75
+
fetchGitHubData();
76
+
}, [currentStep]);
77
+
78
+
// Fetch GitHub organizations directly
79
+
const fetchGitHubOrganizations = async () => {
80
+
setIsLoadingOrgs(true);
81
+
try {
82
+
// Use GitHub API directly - bypassing our backend API
83
+
const accessToken = session?.accessToken;
84
+
85
+
if (!accessToken) {
86
+
console.error('No GitHub access token found in session');
87
+
toast.error('GitHub authorization required');
88
+
return;
89
+
}
90
+
91
+
const response = await fetch('https://api.github.com/user/orgs', {
92
+
headers: {
93
+
Authorization: `Bearer ${accessToken}`,
94
+
Accept: 'application/vnd.github.v3+json'
95
+
}
96
+
});
97
+
98
+
if (!response.ok) {
99
+
throw new Error(`GitHub API error: ${response.status}`);
100
+
}
101
+
102
+
const data = await response.json();
103
+
setOrganizations(data);
104
+
105
+
if (data.length > 0) {
106
+
setSelectedOrg(data[0]);
107
+
fetchGitHubRepositories(data[0].login);
108
+
}
109
+
} catch (error) {
110
+
console.error('Error fetching GitHub organizations:', error);
111
+
toast.error('Failed to load GitHub organizations');
112
+
// Fallback to mocks if needed
113
+
setOrganizations([
114
+
{ id: 1, login: 'Example-Organization', avatar_url: '' }
115
+
]);
116
+
} finally {
117
+
setIsLoadingOrgs(false);
118
+
}
119
+
};
120
+
121
+
// Fetch repositories for the selected organization
122
+
const fetchGitHubRepositories = async (orgName: string) => {
123
+
setIsLoadingRepos(true);
124
+
try {
125
+
// Use GitHub API directly - bypassing our backend API
126
+
const accessToken = session?.accessToken;
127
+
128
+
if (!accessToken) {
129
+
console.error('No GitHub access token found in session');
130
+
toast.error('GitHub authorization required');
131
+
return;
132
+
}
133
+
134
+
const response = await fetch(`https://api.github.com/orgs/${orgName}/repos?per_page=100`, {
135
+
headers: {
136
+
Authorization: `Bearer ${accessToken}`,
137
+
Accept: 'application/vnd.github.v3+json'
138
+
}
139
+
});
140
+
141
+
if (!response.ok) {
142
+
throw new Error(`GitHub API error: ${response.status}`);
143
+
}
144
+
145
+
const data = await response.json();
146
+
setRepositories(data);
147
+
} catch (error) {
148
+
console.error(`Error fetching repositories for ${orgName}:`, error);
149
+
toast.error('Failed to load repositories');
150
+
// Fallback to mocks if needed
151
+
setRepositories([
152
+
{ id: 101, name: 'example-repo', full_name: 'Example-Organization/example-repo', private: false }
153
+
]);
154
+
} finally {
155
+
setIsLoadingRepos(false);
156
+
}
157
+
};
158
+
159
+
// Handle organization selection
160
+
const handleOrgSelect = (org: GitHubOrganization) => {
161
+
setSelectedOrg(org);
162
+
setRepositories([]);
163
+
setSelectedRepoIds(new Set());
164
+
fetchGitHubRepositories(org.login);
165
+
};
166
+
167
+
// Toggle repository selection
168
+
const toggleRepository = (repoId: number) => {
169
+
setSelectedRepoIds(prev => {
170
+
const newSet = new Set(prev);
171
+
if (newSet.has(repoId)) {
172
+
newSet.delete(repoId);
173
+
} else {
174
+
newSet.add(repoId);
175
+
}
176
+
return newSet;
177
+
});
178
+
};
179
+
28
180
// Check if GitHub App is installed (simplified version)
29
181
useEffect(() => {
30
182
// In a real implementation, you'd make an API call to check if the GitHub App
···
56
208
// This is where you'd open the GitHub App installation page
57
209
// For now, we'll use the InstallGitHubApp component's logic
58
210
};
211
+
212
+
// Handle provider selection
213
+
const handleProviderSelect = (provider: 'openai' | 'google' | 'anthropic' | null) => {
214
+
setSelectedProvider(provider);
215
+
setSelectedModelId(null); // Reset model when provider changes
216
+
setApiKey('');
217
+
};
59
218
60
219
// Go to next step
61
-
const handleNext = () => {
220
+
const handleNext = async () => {
62
221
if (currentStep < STEPS.COMPLETE) {
222
+
// Validate AI settings before proceeding
223
+
if (currentStep === STEPS.AI_SETUP) {
224
+
if (!selectedProvider) {
225
+
toast.warning("Please select an AI provider");
226
+
return;
227
+
}
228
+
if (!selectedModelId) {
229
+
toast.warning("Please select an AI model");
230
+
return;
231
+
}
232
+
233
+
// Save AI settings (in a real app, this would be persisted)
234
+
toast.success(`${selectedProvider} provider with ${selectedModelId} model selected`);
235
+
// In a real implementation, you would save the API key and other settings to the database
236
+
}
237
+
238
+
// Handle repository tracking when completing the repo selection step
239
+
if (currentStep === STEPS.REPO_SELECT && selectedRepoIds.size > 0) {
240
+
try {
241
+
// In a real implementation, you would save the selected repositories
242
+
// This is a simplified version that just shows a toast
243
+
toast.success(`${selectedRepoIds.size} repositories selected for tracking`);
244
+
245
+
// Ideally you'd make an API call to update repositories
246
+
// const response = await fetch('/api/repositories/track', {
247
+
// method: 'POST',
248
+
// headers: { 'Content-Type': 'application/json' },
249
+
// body: JSON.stringify({
250
+
// repositoryIds: Array.from(selectedRepoIds),
251
+
// tracked: true
252
+
// })
253
+
// });
254
+
// if (!response.ok) throw new Error('Failed to update repository tracking');
255
+
} catch (error) {
256
+
console.error('Error updating repository tracking:', error);
257
+
toast.error('Failed to update repository tracking');
258
+
return;
259
+
}
260
+
}
261
+
63
262
setCurrentStep(currentStep + 1);
64
263
} else {
65
264
// Onboarding complete, navigate to dashboard
···
75
274
76
275
// Progress percentage
77
276
const progressPercentage = Math.min(100, (currentStep / (STEPS.COMPLETE)) * 100);
277
+
278
+
// Get API key label and link based on provider
279
+
const getApiKeyInfo = () => {
280
+
if (!selectedProvider) return { label: '', link: '' };
281
+
282
+
switch (selectedProvider) {
283
+
case 'openai':
284
+
return {
285
+
label: 'OpenAI API Key',
286
+
link: 'https://platform.openai.com/api-keys'
287
+
};
288
+
case 'google':
289
+
return {
290
+
label: 'Google AI API Key',
291
+
link: 'https://ai.google.dev/'
292
+
};
293
+
case 'anthropic':
294
+
return {
295
+
label: 'Anthropic API Key',
296
+
link: 'https://console.anthropic.com/'
297
+
};
298
+
default:
299
+
return { label: '', link: '' };
300
+
}
301
+
};
302
+
303
+
const apiKeyInfo = getApiKeyInfo();
78
304
79
305
return (
80
306
<div className="flex flex-col items-center justify-center min-h-screen bg-background p-4">
···
82
308
<CardHeader>
83
309
<div className="flex justify-between items-center">
84
310
<div>
85
-
<CardTitle className="text-2xl">Welcome to Pandora</CardTitle>
311
+
<CardTitle className="text-2xl">Welcome to PR Cat</CardTitle>
86
312
<CardDescription>
87
313
Let's get your workspace set up in just a few steps
88
314
</CardDescription>
···
93
319
</div>
94
320
</CardHeader>
95
321
96
-
<Progress value={progressPercentage} className="mb-4 mx-6" />
322
+
<div className="px-6">
323
+
<Progress value={progressPercentage} className="mb-4" />
324
+
</div>
97
325
98
326
<CardContent>
99
327
{currentStep === STEPS.WELCOME && (
100
328
<div className="space-y-4">
101
-
<h3 className="text-lg font-medium">Welcome to Pandora PR Categorizer!</h3>
102
329
<p>
103
-
Pandora helps you categorize and analyze pull requests using AI.
104
-
Let's set up your account in a few simple steps.
330
+
PR Cat helps engineering leads who are in the trenches with their teams. Not an enterprise surveillance tool, but a collaborative platform that improves flow and removes barriers together.
105
331
</p>
106
332
<div className="flex flex-col space-y-2">
107
333
<div className="flex items-center space-x-2 text-sm">
···
134
360
<div className="space-y-4">
135
361
<h3 className="text-lg font-medium">Install GitHub App</h3>
136
362
<p>
137
-
To analyze your pull requests, Pandora needs access to your GitHub repositories.
363
+
To analyze your pull requests, PR Cat needs access to your GitHub repositories.
138
364
Install our GitHub App to continue.
139
365
</p>
140
366
···
147
373
<div className="flex flex-col items-center space-y-4 p-4 bg-muted/50 rounded-md">
148
374
<Github className="h-10 w-10" />
149
375
<p className="text-sm text-center">
150
-
Click the button below to install the Pandora GitHub App.
376
+
Click the button below to install the PR Cat GitHub App.
151
377
You'll be redirected to GitHub to complete the installation.
152
378
</p>
153
379
<InstallGitHubApp onClick={handleGitHubAppInstall} />
···
160
386
<div className="space-y-4">
161
387
<h3 className="text-lg font-medium">Configure AI Settings</h3>
162
388
<p>
163
-
Pandora uses AI to categorize your pull requests. Choose your preferred AI provider and add your API key.
389
+
PR Cat uses AI to categorize your pull requests. Choose your preferred AI provider and add your API key.
164
390
</p>
391
+
392
+
{/* Provider Selection */}
165
393
<div className="space-y-2">
166
-
<p className="text-sm font-medium">Select AI Provider</p>
394
+
<Label htmlFor="provider-select">Select AI Provider</Label>
167
395
<div className="grid grid-cols-3 gap-2">
168
-
<Button variant="outline" className="justify-start h-auto py-2" onClick={() => toast.info("OpenAI selected")}>
396
+
<Button
397
+
variant={selectedProvider === 'openai' ? 'default' : 'outline'}
398
+
className="justify-start h-auto py-2"
399
+
onClick={() => handleProviderSelect('openai')}
400
+
>
169
401
<div className="flex flex-col items-start">
170
402
<span>OpenAI</span>
171
403
<span className="text-xs text-muted-foreground">GPT-4o, GPT-3.5</span>
172
404
</div>
173
405
</Button>
174
-
<Button variant="outline" className="justify-start h-auto py-2" onClick={() => toast.info("Google selected")}>
406
+
<Button
407
+
variant={selectedProvider === 'google' ? 'default' : 'outline'}
408
+
className="justify-start h-auto py-2"
409
+
onClick={() => handleProviderSelect('google')}
410
+
>
175
411
<div className="flex flex-col items-start">
176
412
<span>Google</span>
177
413
<span className="text-xs text-muted-foreground">Gemini</span>
178
414
</div>
179
415
</Button>
180
-
<Button variant="outline" className="justify-start h-auto py-2" onClick={() => toast.info("Anthropic selected")}>
416
+
<Button
417
+
variant={selectedProvider === 'anthropic' ? 'default' : 'outline'}
418
+
className="justify-start h-auto py-2"
419
+
onClick={() => handleProviderSelect('anthropic')}
420
+
>
181
421
<div className="flex flex-col items-start">
182
422
<span>Anthropic</span>
183
423
<span className="text-xs text-muted-foreground">Claude</span>
184
424
</div>
185
425
</Button>
186
426
</div>
187
-
<p className="text-sm text-muted-foreground mt-4">
188
-
You can configure these settings in detail later from the Settings page.
189
-
</p>
190
427
</div>
428
+
429
+
{/* Model Selection - only shown when provider is selected */}
430
+
{selectedProvider && (
431
+
<div className="space-y-2">
432
+
<Label htmlFor="model-select">Select AI Model</Label>
433
+
<Select
434
+
value={selectedModelId || ''}
435
+
onValueChange={(value) => setSelectedModelId(value || null)}
436
+
>
437
+
<SelectTrigger id="model-select">
438
+
<SelectValue placeholder={`Select ${selectedProvider} model`} />
439
+
</SelectTrigger>
440
+
<SelectContent>
441
+
{availableModels.map((model) => (
442
+
<SelectItem key={model.id} value={model.id}>
443
+
{model.name}
444
+
</SelectItem>
445
+
))}
446
+
</SelectContent>
447
+
</Select>
448
+
</div>
449
+
)}
450
+
451
+
{/* API Key Input - only shown when provider is selected */}
452
+
{selectedProvider && (
453
+
<div className="space-y-2">
454
+
<Label htmlFor="api-key-input">{apiKeyInfo.label}</Label>
455
+
<Input
456
+
id="api-key-input"
457
+
type="password"
458
+
placeholder="Enter your API key"
459
+
value={apiKey}
460
+
onChange={(e) => setApiKey(e.target.value)}
461
+
/>
462
+
<p className="text-xs text-muted-foreground">
463
+
Get your API key from <a href={apiKeyInfo.link} target="_blank" rel="noopener noreferrer" className="underline">
464
+
{selectedProvider === 'openai' ? 'OpenAI dashboard' :
465
+
selectedProvider === 'google' ? 'Google AI Studio' : 'Anthropic Console'}
466
+
</a>.
467
+
</p>
468
+
</div>
469
+
)}
470
+
471
+
<p className="text-sm text-muted-foreground mt-4">
472
+
You can configure these settings in detail later from the Settings page.
473
+
</p>
191
474
</div>
192
475
)}
193
476
···
195
478
<div className="space-y-4">
196
479
<h3 className="text-lg font-medium">Select Repositories</h3>
197
480
<p>
198
-
Choose which repositories you want Pandora to analyze and categorize pull requests for.
481
+
Choose which repositories you want PR Cat to analyze and categorize pull requests for.
199
482
</p>
200
-
<div className="p-4 bg-muted/50 rounded-md">
201
-
<p className="text-sm text-center">
202
-
You can select repositories once GitHub organizations have been synced.
203
-
This can be done from the dashboard after setup.
204
-
</p>
205
-
</div>
483
+
484
+
{isLoadingOrgs ? (
485
+
<div className="p-4 bg-muted/50 rounded-md text-center">
486
+
<p>Loading your GitHub organizations...</p>
487
+
</div>
488
+
) : organizations.length === 0 ? (
489
+
<div className="p-4 bg-muted/50 rounded-md">
490
+
<p className="text-sm text-center">
491
+
No GitHub organizations found. Please make sure you've installed the PR Cat GitHub App and have access to GitHub organizations.
492
+
</p>
493
+
</div>
494
+
) : (
495
+
<div className="space-y-4">
496
+
{/* Organization List */}
497
+
<div className="space-y-2">
498
+
<Label>Select Organization</Label>
499
+
<div className="flex flex-wrap gap-2">
500
+
{organizations.map((org) => (
501
+
<Button
502
+
key={org.id}
503
+
variant={selectedOrg?.id === org.id ? "default" : "outline"}
504
+
size="sm"
505
+
onClick={() => handleOrgSelect(org)}
506
+
className="flex items-center gap-2"
507
+
>
508
+
<Avatar className="h-5 w-5">
509
+
<AvatarImage src={org.avatar_url || undefined} alt={org.login} />
510
+
<AvatarFallback>{org.login.charAt(0).toUpperCase()}</AvatarFallback>
511
+
</Avatar>
512
+
<span>{org.login}</span>
513
+
</Button>
514
+
))}
515
+
</div>
516
+
</div>
517
+
518
+
{/* Repository List */}
519
+
{selectedOrg && (
520
+
<div className="space-y-2">
521
+
<Label>Select Repositories to Track</Label>
522
+
{isLoadingRepos ? (
523
+
<p className="text-sm">Loading repositories...</p>
524
+
) : repositories.length === 0 ? (
525
+
<p className="text-sm">No repositories found for this organization.</p>
526
+
) : (
527
+
<div className="max-h-60 overflow-y-auto border rounded-md p-2">
528
+
{repositories.map((repo) => (
529
+
<div
530
+
key={repo.id}
531
+
className="flex items-center p-2 hover:bg-muted/50 rounded-md cursor-pointer"
532
+
onClick={() => toggleRepository(repo.id)}
533
+
>
534
+
<Checkbox
535
+
id={`repo-${repo.id}`}
536
+
checked={selectedRepoIds.has(repo.id)}
537
+
className="mr-2"
538
+
/>
539
+
<Label
540
+
htmlFor={`repo-${repo.id}`}
541
+
className="flex-grow cursor-pointer"
542
+
>
543
+
{repo.name}
544
+
{repo.private && (
545
+
<span className="ml-2 text-xs bg-muted px-1 py-0.5 rounded">Private</span>
546
+
)}
547
+
</Label>
548
+
</div>
549
+
))}
550
+
</div>
551
+
)}
552
+
<p className="text-xs text-muted-foreground">
553
+
Selected {selectedRepoIds.size} repositories for tracking
554
+
</p>
555
+
</div>
556
+
)}
557
+
</div>
558
+
)}
206
559
</div>
207
560
)}
208
561
···
213
566
</div>
214
567
<h3 className="text-lg font-medium">Setup Complete!</h3>
215
568
<p>
216
-
You're all set! Pandora is now ready to help you analyze and categorize your pull requests.
569
+
You're all set! PR Cat is now ready to help you analyze and categorize your pull requests.
217
570
</p>
218
571
</div>
219
572
)}
+7
-21
components/site-header.tsx
+7
-21
components/site-header.tsx
···
1
1
import { Separator } from "@/components/ui/separator"
2
2
import { SidebarTrigger } from "@/components/ui/sidebar"
3
-
import { Button } from "@/components/ui/button"
4
-
import { RefreshCcw, Settings, FileBarChart } from "lucide-react"
3
+
import { PrcatLogo } from "@/components/ui/prcat-logo"
5
4
6
-
export function SiteHeader() {
5
+
export function SiteHeader({ pageTitle }: { pageTitle?: string }) {
7
6
return (
8
7
<header className="flex h-(--header-height) shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)">
9
8
<div className="flex w-full items-center justify-between gap-1 px-4 lg:gap-2 lg:px-6">
···
13
12
orientation="vertical"
14
13
className="mx-2 data-[orientation=vertical]:h-4"
15
14
/>
16
-
<h1 className="text-base font-medium">Dashboard</h1>
17
-
</div>
18
-
19
-
<div className="flex items-center gap-2">
20
-
<Button variant="outline" size="sm" className="gap-1.5">
21
-
<RefreshCcw size={16} />
22
-
<span className="hidden md:inline">Sync GitHub PRs</span>
23
-
</Button>
24
-
25
-
<Button variant="outline" size="sm" className="gap-1.5">
26
-
<FileBarChart size={16} />
27
-
<span className="hidden md:inline">Generate Report</span>
28
-
</Button>
29
-
30
-
<Button variant="outline" size="sm" className="gap-1.5">
31
-
<Settings size={16} />
32
-
<span className="hidden md:inline">Configure Categories</span>
33
-
</Button>
15
+
{pageTitle ? (
16
+
<h1 className="text-2xl font-semibold">{pageTitle}</h1>
17
+
) : (
18
+
<PrcatLogo fontSize="text-base" iconSize="h-4 w-4" />
19
+
)}
34
20
</div>
35
21
</div>
36
22
</header>
+108
-48
components/ui/ai-settings-tab.tsx
+108
-48
components/ui/ai-settings-tab.tsx
···
8
8
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
9
9
import { Input } from '@/components/ui/input';
10
10
import { toast } from 'sonner';
11
-
import { AiSettings as FetchedAiSettings, UpdateAiSettingsPayload } from '@/lib/repositories';
11
+
import { AiSettings as FetchedAiSettings, UpdateAiSettingsPayload, AIProvider } from '@/lib/repositories';
12
12
import { Organization } from '@/lib/types';
13
13
import { Avatar, AvatarImage, AvatarFallback } from './avatar';
14
-
import { allModels, ModelDefinition } from '@/lib/ai-models'; // Import shared models and definition
14
+
import { allModels, ModelDefinition } from '@/lib/ai-models';
15
15
16
16
export function AiSettingsTab() {
17
17
const { data: session, status: sessionStatus } = useSession();
···
20
20
21
21
const [fetchedSettings, setFetchedSettings] = useState<FetchedAiSettings | null>(null);
22
22
23
+
const [selectedProvider, setSelectedProvider] = useState<AIProvider>(null);
23
24
const [selectedModelId, setSelectedModelId] = useState<string | null>(null);
24
25
const [openaiApiKeyInput, setOpenaiApiKeyInput] = useState('');
25
26
const [googleApiKeyInput, setGoogleApiKeyInput] = useState('');
···
27
28
28
29
const [isLoadingSettings, setIsLoadingSettings] = useState(false);
29
30
const [isSaving, setIsSaving] = useState(false);
31
+
32
+
// Filter models by selected provider
33
+
const availableModels = selectedProvider
34
+
? allModels.filter(model => model.provider === selectedProvider)
35
+
: [];
30
36
31
37
useEffect(() => {
32
38
if (session?.organizations && Array.isArray(session.organizations)) {
···
39
45
if (!selectedOrganization) return;
40
46
setIsLoadingSettings(true);
41
47
setFetchedSettings(null);
48
+
setSelectedProvider(null);
42
49
setSelectedModelId(null);
43
50
setOpenaiApiKeyInput('');
44
51
setGoogleApiKeyInput('');
···
50
57
}
51
58
const data: FetchedAiSettings = await response.json();
52
59
setFetchedSettings(data);
60
+
setSelectedProvider(data.provider);
53
61
setSelectedModelId(data.selectedModelId);
54
62
} catch (error) {
55
63
toast.error(error instanceof Error ? error.message : 'Could not load AI settings.');
···
61
69
fetchSettings();
62
70
} else {
63
71
setFetchedSettings(null);
72
+
setSelectedProvider(null);
64
73
setSelectedModelId(null);
65
74
setOpenaiApiKeyInput('');
66
75
setGoogleApiKeyInput('');
···
69
78
}
70
79
}, [selectedOrganization]);
71
80
72
-
const currentSelectedModelDetails = selectedModelId ? allModels.find(m => m.id === selectedModelId) : null;
81
+
// When provider changes, reset model selection
82
+
useEffect(() => {
83
+
if (selectedProvider) {
84
+
// Check if current selected model is from the new provider
85
+
const currentModelMatchesProvider = selectedModelId &&
86
+
allModels.some(m => m.id === selectedModelId && m.provider === selectedProvider);
87
+
88
+
// If not, clear the model selection
89
+
if (!currentModelMatchesProvider) {
90
+
setSelectedModelId(null);
91
+
}
92
+
}
93
+
}, [selectedProvider, selectedModelId]);
73
94
74
95
const handleSave = async () => {
75
96
if (!selectedOrganization) return;
76
97
setIsSaving(true);
77
98
78
99
const payload: UpdateAiSettingsPayload = {
100
+
provider: selectedProvider,
79
101
selectedModelId: selectedModelId,
80
102
};
81
103
82
-
const modelDetails = selectedModelId ? allModels.find(m => m.id === selectedModelId) : null;
83
-
84
-
if (modelDetails) {
104
+
// Handle API key changes based on provider
105
+
if (selectedProvider) {
85
106
let currentInput = '';
86
107
let apiKeyCurrentlySet = false;
87
108
let apiKeyPayloadKey: keyof UpdateAiSettingsPayload | null = null;
88
109
89
-
switch (modelDetails.provider) {
110
+
switch (selectedProvider) {
90
111
case 'openai':
91
112
currentInput = openaiApiKeyInput;
92
113
apiKeyCurrentlySet = !!fetchedSettings?.isOpenAiKeySet;
···
137
158
const fetchResponse = await fetch(`/api/organizations/${selectedOrganization.id}/ai-settings`);
138
159
const data: FetchedAiSettings = await fetchResponse.json();
139
160
setFetchedSettings(data);
161
+
setSelectedProvider(data.provider);
140
162
setSelectedModelId(data.selectedModelId);
141
163
setOpenaiApiKeyInput('');
142
164
setGoogleApiKeyInput('');
···
247
269
</CardDescription>
248
270
</CardHeader>
249
271
<CardContent className="space-y-6">
272
+
{/* Provider Selection */}
250
273
<div className="space-y-2">
251
-
<Label htmlFor="ai-model-select">AI Model</Label>
274
+
<Label htmlFor="provider-select">AI Provider</Label>
252
275
<Select
253
-
value={selectedModelId === null ? '__none__' : selectedModelId}
276
+
value={selectedProvider || ''}
254
277
onValueChange={(value) => {
255
-
const newModelId = value === '__none__' ? null : value;
256
-
setSelectedModelId(newModelId);
257
-
setOpenaiApiKeyInput('');
258
-
setGoogleApiKeyInput('');
259
-
setAnthropicApiKeyInput('');
278
+
if (value === '') {
279
+
setSelectedProvider(null);
280
+
} else {
281
+
setSelectedProvider(value as AIProvider);
282
+
}
260
283
}}
261
284
>
262
-
<SelectTrigger id="ai-model-select">
263
-
<SelectValue placeholder="Select a model (or None)" />
285
+
<SelectTrigger id="provider-select">
286
+
<SelectValue placeholder="Select an AI provider" />
264
287
</SelectTrigger>
265
288
<SelectContent>
266
-
<SelectItem value="__none__">None (Disable AI Categorization)</SelectItem>
267
-
{allModels.map(model => (
268
-
<SelectItem key={model.id} value={model.id}>
269
-
{model.providerName}: {model.name}
270
-
</SelectItem>
271
-
))}
289
+
<SelectItem value="">None (Disable AI Categorization)</SelectItem>
290
+
<SelectItem value="openai">OpenAI</SelectItem>
291
+
<SelectItem value="google">Google (Gemini)</SelectItem>
292
+
<SelectItem value="anthropic">Anthropic (Claude)</SelectItem>
272
293
</SelectContent>
273
294
</Select>
274
295
</div>
296
+
297
+
{/* Model Selection - only shown when provider is selected */}
298
+
{selectedProvider && (
299
+
<div className="space-y-2">
300
+
<Label htmlFor="model-select">AI Model</Label>
301
+
<Select
302
+
value={selectedModelId || ''}
303
+
onValueChange={(value) => setSelectedModelId(value || null)}
304
+
>
305
+
<SelectTrigger id="model-select">
306
+
<SelectValue placeholder={`Select ${selectedProvider} model`} />
307
+
</SelectTrigger>
308
+
<SelectContent>
309
+
{availableModels.map(model => (
310
+
<SelectItem key={model.id} value={model.id}>
311
+
{model.name}
312
+
</SelectItem>
313
+
))}
314
+
</SelectContent>
315
+
</Select>
316
+
</div>
317
+
)}
275
318
276
-
{currentSelectedModelDetails && (
319
+
{/* API key input - show based on selected provider */}
320
+
{selectedProvider === 'openai' && (
277
321
<div className="space-y-2">
278
-
<Label htmlFor={`api-key-${currentSelectedModelDetails.provider}`}>
279
-
{currentSelectedModelDetails.providerName} API Key
322
+
<Label htmlFor="openai-key">
323
+
OpenAI API Key
324
+
{fetchedSettings?.isOpenAiKeySet && <span className="ml-2 text-xs text-muted-foreground">(Already set)</span>}
280
325
</Label>
281
326
<Input
282
-
id={`api-key-${currentSelectedModelDetails.provider}`}
327
+
id="openai-key"
283
328
type="password"
284
-
value={getApiKeyInputProps(currentSelectedModelDetails.provider).value}
285
-
onChange={getApiKeyInputProps(currentSelectedModelDetails.provider).onChange}
286
-
placeholder={getApiKeyInputProps(currentSelectedModelDetails.provider).placeholder}
329
+
placeholder={getApiKeyInputProps('openai').placeholder}
330
+
value={openaiApiKeyInput}
331
+
onChange={getApiKeyInputProps('openai').onChange}
287
332
/>
288
-
{getApiKeyInputProps(currentSelectedModelDetails.provider).isSet &&
289
-
!getApiKeyInputProps(currentSelectedModelDetails.provider).value && (
290
-
<p className="text-xs text-muted-foreground">
291
-
An API key is currently set for {currentSelectedModelDetails.providerName}.
292
-
To update it, enter the new key.
293
-
To clear this key, leave this field blank and click "Save AI Settings".
294
-
</p>
295
-
)}
296
-
{!getApiKeyInputProps(currentSelectedModelDetails.provider).isSet &&
297
-
currentSelectedModelDetails.provider && ( // Ensure provider exists to show specific message
298
-
<p className="text-xs text-muted-foreground">
299
-
Enter your {currentSelectedModelDetails.providerName} API key.
300
-
</p>
301
-
)
302
-
}
333
+
<p className="text-xs text-muted-foreground">Get your API key from <a href="https://platform.openai.com/api-keys" target="_blank" rel="noopener noreferrer" className="underline">OpenAI dashboard</a>.</p>
303
334
</div>
304
335
)}
305
-
306
-
{selectedModelId && !currentSelectedModelDetails && (
307
-
<p className="text-destructive text-xs">Error: Selected model details not found. Please re-select.</p>
336
+
337
+
{selectedProvider === 'google' && (
338
+
<div className="space-y-2">
339
+
<Label htmlFor="google-key">
340
+
Google AI API Key
341
+
{fetchedSettings?.isGoogleKeySet && <span className="ml-2 text-xs text-muted-foreground">(Already set)</span>}
342
+
</Label>
343
+
<Input
344
+
id="google-key"
345
+
type="password"
346
+
placeholder={getApiKeyInputProps('google').placeholder}
347
+
value={googleApiKeyInput}
348
+
onChange={getApiKeyInputProps('google').onChange}
349
+
/>
350
+
<p className="text-xs text-muted-foreground">Get your API key from <a href="https://ai.google.dev/" target="_blank" rel="noopener noreferrer" className="underline">Google AI Studio</a>.</p>
351
+
</div>
308
352
)}
309
353
354
+
{selectedProvider === 'anthropic' && (
355
+
<div className="space-y-2">
356
+
<Label htmlFor="anthropic-key">
357
+
Anthropic API Key
358
+
{fetchedSettings?.isAnthropicKeySet && <span className="ml-2 text-xs text-muted-foreground">(Already set)</span>}
359
+
</Label>
360
+
<Input
361
+
id="anthropic-key"
362
+
type="password"
363
+
placeholder={getApiKeyInputProps('anthropic').placeholder}
364
+
value={anthropicApiKeyInput}
365
+
onChange={getApiKeyInputProps('anthropic').onChange}
366
+
/>
367
+
<p className="text-xs text-muted-foreground">Get your API key from <a href="https://console.anthropic.com/" target="_blank" rel="noopener noreferrer" className="underline">Anthropic Console</a>.</p>
368
+
</div>
369
+
)}
310
370
</CardContent>
311
371
<CardFooter>
312
-
<Button onClick={handleSave} disabled={isSaving || isLoadingSettings || !selectedOrganization}>
372
+
<Button onClick={handleSave} disabled={isSaving}>
313
373
{isSaving ? 'Saving...' : 'Save AI Settings'}
314
374
</Button>
315
375
</CardFooter>
+2
-2
components/ui/prcat-logo.tsx
+2
-2
components/ui/prcat-logo.tsx
···
19
19
return (
20
20
<div className={cn("inline-flex flex-row items-center", className)}>
21
21
<div className="flex flex-row items-center">
22
-
<span className={cn("inline-block", fontSize, dark ? "text-white" : "text-foreground")}>pr</span>
22
+
<span className={cn("inline-block font-bold", fontSize, dark ? "text-white" : "text-foreground")}>PR</span>
23
23
<svg
24
24
xmlns="http://www.w3.org/2000/svg"
25
25
fill="none"
···
37
37
clipRule="evenodd"
38
38
/>
39
39
</svg>
40
-
<span className={cn("inline-block", fontSize, dark ? "text-white" : "text-foreground")}>cat</span>
40
+
<span className={cn("inline-block font-bold", fontSize, dark ? "text-white" : "text-foreground")}>Cat</span>
41
41
</div>
42
42
</div>
43
43
);
+6
-1
components/ui/setup-status-alert.tsx
+6
-1
components/ui/setup-status-alert.tsx
···
5
5
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
6
6
import { Button } from '@/components/ui/button';
7
7
import { useRouter } from 'next/navigation';
8
+
import { PrcatLogo } from '@/components/ui/prcat-logo';
8
9
9
10
interface SetupStatusAlertProps {
10
11
className?: string;
···
18
19
<AlertCircle className="h-4 w-4" />
19
20
<AlertTitle>Setup Incomplete</AlertTitle>
20
21
<AlertDescription className="flex justify-between items-center">
21
-
<span>Your Pandora setup is incomplete. Complete the setup to enable PR categorization.</span>
22
+
<div className="flex items-center">
23
+
<span>Your </span>
24
+
<PrcatLogo className="mx-1" iconSize="h-3 w-3" fontSize="text-sm" />
25
+
<span> setup is incomplete. Complete the setup to enable PR categorization.</span>
26
+
</div>
22
27
<Button
23
28
size="sm"
24
29
onClick={() => router.push('/onboarding')}
+21
-13
lib/repositories/settings-repository.ts
+21
-13
lib/repositories/settings-repository.ts
···
1
1
import { query, execute } from '@/lib/db';
2
2
import { Setting } from '@/lib/types';
3
3
4
-
// Old keys (will be replaced or unused for AI settings)
5
-
// const AI_PROVIDER_KEY = 'ai_provider';
6
-
// const AI_MODEL_ID_KEY = 'ai_model_id';
7
-
8
-
// New keys for AI settings
4
+
// Keys for AI settings
5
+
const AI_PROVIDER_KEY = 'ai_provider';
9
6
const AI_SELECTED_MODEL_ID_KEY = 'ai_selected_model_id';
10
7
const AI_OPENAI_API_KEY_KEY = 'ai_openai_api_key';
11
-
const AI_GOOGLE_API_KEY_KEY = 'ai_google_api_key'; // Or GEMINI_API_KEY if that naming is preferred
8
+
const AI_GOOGLE_API_KEY_KEY = 'ai_google_api_key';
12
9
const AI_ANTHROPIC_API_KEY_KEY = 'ai_anthropic_api_key';
13
10
11
+
// Valid AI provider values
12
+
export type AIProvider = 'openai' | 'google' | 'anthropic' | null;
13
+
14
14
// Interface for data returned to the client (keys are not sent)
15
-
export interface AiSettings { // Renamed from GetAiSettingsResponse for brevity in frontend
15
+
export interface AiSettings {
16
+
provider: AIProvider;
16
17
selectedModelId: string | null;
17
18
isOpenAiKeySet: boolean;
18
19
isGoogleKeySet: boolean;
···
21
22
22
23
// Interface for payload when updating settings (actual keys are sent)
23
24
export interface UpdateAiSettingsPayload {
24
-
selectedModelId?: string | null; // To change the model or disable AI (null)
25
-
openaiApiKey?: string | null; // To set or clear/remove a key. Null to clear.
25
+
provider?: AIProvider;
26
+
selectedModelId?: string | null;
27
+
openaiApiKey?: string | null;
26
28
googleApiKey?: string | null;
27
29
anthropicApiKey?: string | null;
28
30
}
···
51
53
}
52
54
53
55
export async function getOrganizationAiSettings(organizationId: number): Promise<AiSettings> {
56
+
const provider = await getOrganizationSetting(organizationId, AI_PROVIDER_KEY) as AIProvider;
54
57
const selectedModelId = await getOrganizationSetting(organizationId, AI_SELECTED_MODEL_ID_KEY);
55
58
const openAiKey = await getOrganizationSetting(organizationId, AI_OPENAI_API_KEY_KEY);
56
59
const googleKey = await getOrganizationSetting(organizationId, AI_GOOGLE_API_KEY_KEY);
57
60
const anthropicKey = await getOrganizationSetting(organizationId, AI_ANTHROPIC_API_KEY_KEY);
58
61
59
62
return {
63
+
provider: provider || null,
60
64
selectedModelId,
61
-
isOpenAiKeySet: !!openAiKey, // True if key exists and is not empty string (could refine if empty string is valid)
65
+
isOpenAiKeySet: !!openAiKey,
62
66
isGoogleKeySet: !!googleKey,
63
67
isAnthropicKeySet: !!anthropicKey,
64
68
};
···
68
72
organizationId: number,
69
73
payload: UpdateAiSettingsPayload
70
74
): Promise<void> {
75
+
if (payload.provider !== undefined) {
76
+
await updateOrganizationSetting(organizationId, AI_PROVIDER_KEY, payload.provider);
77
+
}
71
78
if (payload.selectedModelId !== undefined) {
72
79
await updateOrganizationSetting(organizationId, AI_SELECTED_MODEL_ID_KEY, payload.selectedModelId);
73
80
}
···
82
89
}
83
90
}
84
91
85
-
// New function to get a specific API key for an organization and provider
86
-
export async function getOrganizationApiKey(organizationId: number, provider: 'openai' | 'google' | 'anthropic'): Promise<string | null> {
92
+
// Get a specific API key for an organization and provider
93
+
export async function getOrganizationApiKey(organizationId: number, provider: AIProvider): Promise<string | null> {
94
+
if (!provider) return null;
95
+
87
96
let apiKeyDBKey = '';
88
97
switch (provider) {
89
98
case 'openai':
···
96
105
apiKeyDBKey = AI_ANTHROPIC_API_KEY_KEY;
97
106
break;
98
107
default:
99
-
// Should not happen with TypeScript, but good for safety
100
108
console.error(`Invalid provider specified for getOrganizationApiKey: ${provider}`);
101
109
return null;
102
110
}