feat(settings): add AI Assistant configuration panel

ShopSettings.razor: new "AI Assistant" section with:
- Enable/disable toggle
- Provider dropdown (OpenAI / OpenRouter / Claude)
- Model input with per-provider placeholder
- API Key input (password with show/hide toggle)
- Per-provider hint text for getting API keys
- System Prompt textarea (optional, default used if empty)
- Save button with success/error feedback
- Config loads on init, saves via BFF PUT /api/bff/ai/config

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ho Ngoc Hai
2026-03-26 18:12:11 +07:00
parent b589752b51
commit dae8aef31f

View File

@@ -78,6 +78,80 @@
}
</div>
@* ─── EN: AI Assistant Configuration ─── *@
@* ─── VI: Cấu hình AI Assistant ─── *@
<div class="admin-panel" style="margin-top:16px;">
<div class="admin-panel__header">
<h3 class="admin-panel__title" style="display:flex;align-items:center;gap:8px;">
<i data-lucide="bot" style="width:18px;height:18px;color:var(--admin-orange-primary);"></i>
AI Assistant
</h3>
</div>
<div class="admin-panel__body" style="display:flex;flex-direction:column;gap:16px;">
<div style="display:flex;justify-content:space-between;align-items:center;">
<div>
<div style="font-size:14px;font-weight:600;">Kích hoạt AI Assistant</div>
<div style="font-size:12px;color:var(--admin-text-tertiary);">Cho phép sử dụng trợ lý AI trong quản lý cửa hàng</div>
</div>
<MudSwitch T="bool" @bind-Value="_aiEnabled" Color="Color.Primary" />
</div>
@if (_aiEnabled)
{
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">
<div>
<label style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;">Provider</label>
<select @bind="_aiProvider" style="width:100%;padding:10px 14px;border-radius:8px;background:var(--admin-bg-elevated);border:1px solid var(--admin-border-subtle);color:var(--admin-text-primary);font-size:14px;">
<option value="openai">OpenAI</option>
<option value="openrouter">OpenRouter</option>
<option value="claude">Claude (Anthropic)</option>
</select>
</div>
<div>
<label style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;">Model</label>
<input type="text" @bind="_aiModel" placeholder="@(_aiProvider switch { "openai" => "gpt-4o", "openrouter" => "anthropic/claude-sonnet-4", "claude" => "claude-sonnet-4-20250514", _ => "gpt-4o" })"
style="width:100%;padding:10px 14px;border-radius:8px;background:var(--admin-bg-elevated);border:1px solid var(--admin-border-subtle);color:var(--admin-text-primary);font-size:14px;" />
</div>
</div>
<div>
<label style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;">API Key</label>
<div style="position:relative;">
<input type="@(_showApiKey ? "text" : "password")" @bind="_aiApiKey" placeholder="sk-..."
style="width:100%;padding:10px 14px;padding-right:44px;border-radius:8px;background:var(--admin-bg-elevated);border:1px solid var(--admin-border-subtle);color:var(--admin-text-primary);font-size:14px;font-family:monospace;" />
<button @onclick="() => _showApiKey = !_showApiKey" style="position:absolute;right:8px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;color:var(--admin-text-tertiary);padding:4px;">
<i data-lucide="@(_showApiKey ? "eye-off" : "eye")" style="width:16px;height:16px;"></i>
</button>
</div>
<div style="font-size:11px;color:var(--admin-text-tertiary);margin-top:4px;">
@(_aiProvider switch {
"openai" => "Lấy API key tại platform.openai.com/api-keys",
"openrouter" => "Lấy API key tại openrouter.ai/keys",
"claude" => "Lấy API key tại console.anthropic.com/settings/keys",
_ => ""
})
</div>
</div>
<div>
<label style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;">System Prompt (tuỳ chọn)</label>
<textarea @bind="_aiSystemPrompt" rows="3" placeholder="Để trống sẽ dùng prompt mặc định cho quản lý cửa hàng..."
style="width:100%;padding:10px 14px;border-radius:8px;background:var(--admin-bg-elevated);border:1px solid var(--admin-border-subtle);color:var(--admin-text-primary);font-size:13px;resize:vertical;min-height:60px;"></textarea>
</div>
<div style="display:flex;align-items:center;gap:12px;">
<button class="admin-btn-primary" @onclick="SaveAiConfig" disabled="@_aiSaving" style="display:inline-flex;align-items:center;gap:8px;">
@if (_aiSaving) { <span>Đang lưu...</span> } else { <i data-lucide="save" style="width:16px;height:16px;"></i> <span>Lưu cấu hình AI</span> }
</button>
@if (_aiMessage != null)
{
<span style="font-size:13px;color:@(_aiSuccess ? "#22C55E" : "#EF4444");">@_aiMessage</span>
}
</div>
}
</div>
</div>
@* ─── EN: Publish / Activate shop (when Draft) ─── *@
@* ─── VI: Kích hoạt cửa hàng (khi đang ở trạng thái Draft) ─── *@
@if (ShopVerticalHelper.IsSetup(ShopStatus))
@@ -187,7 +261,49 @@
protected override async Task OnInitializedAsync()
{
if (ShopId != Guid.Empty)
{
await LoadShopSettings();
await LoadAiConfig();
}
}
private async Task LoadAiConfig()
{
try
{
var config = await DataService.GetAiChatConfigAsync(ShopId);
if (config != null)
{
_aiEnabled = config.Enabled;
_aiProvider = config.Provider ?? "openai";
_aiApiKey = config.ApiKeyMasked ?? "";
_aiModel = config.Model ?? "";
_aiSystemPrompt = config.SystemPrompt ?? "";
}
}
catch { /* AI config not found — defaults are fine */ }
}
private async Task SaveAiConfig()
{
_aiSaving = true;
_aiMessage = null;
StateHasChanged();
try
{
var ok = await DataService.UpdateAiChatConfigAsync(ShopId, new
{
provider = _aiProvider,
apiKey = _aiApiKey,
model = string.IsNullOrWhiteSpace(_aiModel) ? null : _aiModel,
systemPrompt = string.IsNullOrWhiteSpace(_aiSystemPrompt) ? null : _aiSystemPrompt,
enabled = _aiEnabled
});
_aiSuccess = ok;
_aiMessage = ok ? "Đã lưu cấu hình AI!" : "Lỗi khi lưu.";
}
catch (Exception ex) { _aiSuccess = false; _aiMessage = $"Lỗi: {ex.Message}"; }
finally { _aiSaving = false; StateHasChanged(); }
}
private async Task LoadShopSettings()
@@ -242,6 +358,18 @@
StateHasChanged();
}
// EN: AI config state
// VI: Trạng thái cấu hình AI
private bool _aiEnabled;
private string _aiProvider = "openai";
private string _aiApiKey = "";
private string _aiModel = "";
private string _aiSystemPrompt = "";
private bool _showApiKey;
private bool _aiSaving;
private string? _aiMessage;
private bool _aiSuccess;
// EN: Publish state
// VI: Trạng thái kích hoạt
private bool _isPublishing;