feat(church-profile): AI 設定 tab (provider/model/key) with masked keys
This commit is contained in:
@@ -49,6 +49,11 @@ export interface ChurchProfileDto {
|
||||
email: string | null; website: string | null; address: string | null; city: string | null;
|
||||
state: string | null; zipCode: string | null; bankName: string | null;
|
||||
bankAccountNumber: string | null; bankRoutingNumber: string | null; nextCheckNumber: number;
|
||||
aiProvider: string;
|
||||
claudeModel: string | null; claudeApiKeyMasked: string | null;
|
||||
geminiModel: string | null; geminiApiKeyMasked: string | null;
|
||||
}
|
||||
|
||||
export type UpdateChurchProfileRequest = Omit<ChurchProfileDto, 'id'>;
|
||||
export type UpdateChurchProfileRequest =
|
||||
Omit<ChurchProfileDto, 'id' | 'claudeApiKeyMasked' | 'geminiApiKeyMasked'>
|
||||
& { claudeApiKey: string | null; geminiApiKey: string | null };
|
||||
|
||||
+47
@@ -82,5 +82,52 @@
|
||||
<app-notification-settings-tab></app-notification-settings-tab>
|
||||
</ng-template>
|
||||
</kendo-tabstrip-tab>
|
||||
|
||||
<!-- ── Tab 4: AI Settings (Settings permission) ───────────────────────── -->
|
||||
<kendo-tabstrip-tab title="AI 設定 / AI Settings" *appHasPermission="settingsPermission">
|
||||
<ng-template kendoTabContent>
|
||||
<div *ngIf="model" class="max-w-3xl pt-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-3">
|
||||
<label class="flex flex-col gap-1 md:col-span-2">
|
||||
AI Provider / AI 供應商
|
||||
<kendo-dropdownlist
|
||||
[data]="aiProviders" textField="text" valueField="value" [valuePrimitive]="true"
|
||||
[(ngModel)]="model.aiProvider"></kendo-dropdownlist>
|
||||
</label>
|
||||
|
||||
<label class="flex flex-col gap-1">
|
||||
Claude Model / Claude 模型
|
||||
<kendo-textbox [(ngModel)]="model.claudeModel"></kendo-textbox>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
Claude API Key / Claude 金鑰
|
||||
<input kendoTextBox type="password" autocomplete="new-password"
|
||||
[(ngModel)]="claudeApiKeyInput"
|
||||
[placeholder]="model.claudeApiKeyMasked || 'Enter key / 輸入金鑰'" />
|
||||
</label>
|
||||
|
||||
<label class="flex flex-col gap-1">
|
||||
Gemini Model / Gemini 模型
|
||||
<kendo-textbox [(ngModel)]="model.geminiModel"></kendo-textbox>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
Gemini API Key / Gemini 金鑰
|
||||
<input kendoTextBox type="password" autocomplete="new-password"
|
||||
[(ngModel)]="geminiApiKeyInput"
|
||||
[placeholder]="model.geminiApiKeyMasked || 'Enter key / 輸入金鑰'" />
|
||||
</label>
|
||||
|
||||
<p class="md:col-span-2 text-sm" style="color:#6b7280;">
|
||||
Leave a key blank to keep the saved one. / 金鑰留空表示沿用已儲存的設定。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3 mt-4">
|
||||
<button kendoButton themeColor="primary" [disabled]="saving" (click)="save()">Save / 儲存</button>
|
||||
<span class="text-sm" style="color:#065f46;">{{ savedMsg }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</kendo-tabstrip-tab>
|
||||
</kendo-tabstrip>
|
||||
</div>
|
||||
|
||||
+26
-4
@@ -4,8 +4,9 @@ import { FormsModule } from '@angular/forms';
|
||||
import { ButtonsModule } from '@progress/kendo-angular-buttons';
|
||||
import { InputsModule } from '@progress/kendo-angular-inputs';
|
||||
import { LayoutModule } from '@progress/kendo-angular-layout';
|
||||
import { DropDownsModule } from '@progress/kendo-angular-dropdowns';
|
||||
import { DisbursementApiService } from '../../services/disbursement-api.service';
|
||||
import { ChurchProfileDto } from '../../models/disbursement.model';
|
||||
import { ChurchProfileDto, UpdateChurchProfileRequest } from '../../models/disbursement.model';
|
||||
import { HasPermissionDirective } from '../../../../core/directives/has-permission.directive';
|
||||
import { PermissionModules } from '../../../../core/models/permission.model';
|
||||
import { SiteSettingsTabComponent } from '../../../settings/components/site-settings-tab/site-settings-tab.component';
|
||||
@@ -15,7 +16,7 @@ import { NotificationSettingsTabComponent } from '../../../settings/components/n
|
||||
selector: 'app-church-profile-page',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule, FormsModule, ButtonsModule, InputsModule, LayoutModule,
|
||||
CommonModule, FormsModule, ButtonsModule, InputsModule, LayoutModule, DropDownsModule,
|
||||
HasPermissionDirective, SiteSettingsTabComponent, NotificationSettingsTabComponent,
|
||||
],
|
||||
templateUrl: './church-profile-page.component.html',
|
||||
@@ -25,6 +26,15 @@ export class ChurchProfilePageComponent implements OnInit {
|
||||
saving = false;
|
||||
savedMsg = '';
|
||||
|
||||
/** Bound to the password inputs; blank means "keep the saved key". Reset after each save. */
|
||||
claudeApiKeyInput = '';
|
||||
geminiApiKeyInput = '';
|
||||
|
||||
readonly aiProviders = [
|
||||
{ text: 'Claude', value: 'Claude' },
|
||||
{ text: 'Gemini', value: 'Gemini' },
|
||||
];
|
||||
|
||||
/** Settings module gates the Site / Notification tabs. */
|
||||
readonly settingsPermission = { module: PermissionModules.Settings, action: 'read' as const };
|
||||
|
||||
@@ -38,9 +48,21 @@ export class ChurchProfilePageComponent implements OnInit {
|
||||
if (!this.model || this.saving) return;
|
||||
this.saving = true;
|
||||
this.savedMsg = '';
|
||||
const { id, ...req } = this.model;
|
||||
const { id, claudeApiKeyMasked, geminiApiKeyMasked, ...rest } = this.model;
|
||||
const req: UpdateChurchProfileRequest = {
|
||||
...rest,
|
||||
claudeApiKey: this.claudeApiKeyInput.trim() || null,
|
||||
geminiApiKey: this.geminiApiKeyInput.trim() || null,
|
||||
};
|
||||
this.api.updateChurchProfile(req).subscribe({
|
||||
next: () => { this.saving = false; this.savedMsg = 'Saved / 已儲存'; },
|
||||
next: () => {
|
||||
this.saving = false;
|
||||
this.savedMsg = 'Saved / 已儲存';
|
||||
// Clear the key inputs and reload so the masked placeholders reflect the new keys.
|
||||
this.claudeApiKeyInput = '';
|
||||
this.geminiApiKeyInput = '';
|
||||
this.api.getChurchProfile().subscribe(p => (this.model = p));
|
||||
},
|
||||
error: () => {
|
||||
// Error message is shown globally by httpErrorInterceptor.
|
||||
this.saving = false;
|
||||
|
||||
Reference in New Issue
Block a user