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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user