Frontend: Services¶
Visão Geral¶
Services do frontend em Ciba.Web/Services/. Encapsulam lógica de comunicação e funcionalidades transversais.
| Service | Responsabilidade |
|---|---|
ApiClient |
Comunicação HTTP com backend |
AuthService |
Autenticação e sessão |
SignalRService |
Real-time via WebSocket |
DialogHelper |
Abertura de dialogs |
ConfirmDeleteHelper |
Confirmação de exclusão |
SnackbarExtensions |
Feedback de erros |
StatusFormatService |
Formatação de status |
WhatsAppFormatter |
Formatação WhatsApp → HTML |
SubscriptionStateService |
Cache client-side de subscription |
PlaygroundStorageService |
Persistência de chat do Playground em localStorage |
AuthorizationMessageHandler |
Injeção de JWT nas requests |
ApiClient¶
Client HTTP para comunicação com a API. Única forma de acessar o backend.
Arquivo: Services/ApiClient.cs
Características¶
- Retorna
Result<T>ouResult(Result pattern do Shared) - Usa rotas de
ApiRoutes(nunca strings literais) - Deserializa erros automaticamente para
Error - Tratamento de exceções para
Errors.Internal
Métodos Auxiliares¶
private async Task<Result<T>> GetAsync<T>(string url);
private async Task<Result<T>> PostAsync<T>(string url, object? body = null);
private async Task<Result> PostAsync(string url, object? body = null);
private async Task<Result<T>> PutAsync<T>(string url, object? body = null);
private async Task<Result> PutAsync(string url, object? body = null);
private async Task<Result> DeleteAsync(string url);
private async Task<Result<T>> DeleteWithResponseAsync<T>(string url);
Endpoints Disponíveis¶
Autenticação:
Usuarios:
Task<Result<PagedResult<UserListResponse>>> GetUsersAsync(int page, int pageSize, string? search)
Task<Result<CreateUserResponse>> CreateUserAsync(CreateUserRequest request)
Task<Result<UpdateUserResponse>> UpdateUserAsync(Guid id, UpdateUserRequest request)
Task<Result<UpdateUserStatusResponse>> UpdateUserStatusAsync(Guid id, UpdateUserStatusRequest request)
Task<Result> ResetUserPasswordAsync(Guid id, ResetUserPasswordRequest request)
Task<Result> ChangeOwnPasswordAsync(ChangeOwnPasswordRequest request)
Agentes:
Task<Result<List<AgentListResponse>>> GetAgentsAsync(bool includeArchived = false)
Task<Result<AgentDetailResponse>> GetAgentByIdAsync(Guid id)
Task<Result<CreateAgentResponse>> CreateAgentAsync(CreateAgentRequest request)
Task<Result<UpdateAgentResponse>> UpdateAgentAsync(Guid id, UpdateAgentRequest request)
Task<Result> DeleteAgentAsync(Guid id)
Task<Result> RestoreAgentAsync(Guid id)
Task<Result> HardDeleteAgentAsync(Guid id, string confirmationName)
Playground:
Task<Result<PlaygroundChatResponse>> SendPlaygroundMessageAsync(Guid agentId, List<PlaygroundMessageDto> messages)
Task<Result<PlaygroundTranscribeResponse>> TranscribePlaygroundAudioAsync(Guid agentId, string audioBase64, string mimeType)
Task<Result<PlaygroundDescribeImageResponse>> DescribePlaygroundImageAsync(Guid agentId, string imageBase64, string mimeType, string? caption)
Agent Messages:
Task<Result<AgentMessagesResponse>> GetAgentMessagesAsync(Guid agentId)
Task<Result<AgentMessagesResponse>> UpdateAgentMessagesAsync(Guid agentId, UpdateAgentMessagesRequest request)
Agent AI Assistant:
Task<Result<AgentAiAssistantResponse>> SendAiAssistantMessageAsync(Guid agentId, AgentAiAssistantRequest request)
Agent Attachments:
Task<Result<List<AttachmentResponse>>> GetAttachmentsAsync(Guid agentId)
Task<Result<AttachmentResponse>> UploadAttachmentAsync(Guid agentId, UploadAttachmentRequest request)
Task<Result<AttachmentResponse>> UpdateAttachmentAsync(Guid agentId, Guid id, UpdateAttachmentRequest request)
Task<Result> DeleteAttachmentAsync(Guid agentId, Guid id)
Task<Result<AttachmentResponse>> ToggleAttachmentAsync(Guid agentId, Guid id)
Agent Steps:
Task<Result<List<AgentStepResponse>>> GetAgentStepsAsync(Guid agentId)
Task<Result<CreateAgentStepResponse>> CreateAgentStepAsync(Guid agentId, CreateAgentStepRequest request)
Task<Result> UpdateAgentStepAsync(Guid agentId, Guid stepId, UpdateAgentStepRequest request)
Task<Result> DeleteAgentStepAsync(Guid agentId, Guid stepId)
Task<Result> ReorderAgentStepsAsync(Guid agentId, ReorderAgentStepsRequest request)
Task<Result> ToggleAgentStepAsync(Guid agentId, Guid stepId)
Prompt Versions:
Task<Result<List<PromptVersionResponse>>> GetAgentPromptVersionsAsync(Guid agentId)
Task<Result<List<PromptVersionResponse>>> GetStepPromptVersionsAsync(Guid agentId, Guid stepId)
Task<Result> RestorePromptVersionAsync(Guid agentId, Guid versionId)
Agent Follow-Up:
Task<Result<FollowUpConfigResponse?>> GetAgentFollowUpAsync(Guid agentId)
Task<Result<FollowUpConfigResponse>> UpsertAgentFollowUpAsync(Guid agentId, UpsertAgentFollowUpRequest request)
Task<Result> DeleteAgentFollowUpAsync(Guid agentId)
Task<Result<FollowUpConfigResponse?>> GetStepFollowUpAsync(Guid agentId, Guid stepId)
Task<Result<FollowUpConfigResponse>> UpsertStepFollowUpAsync(Guid agentId, Guid stepId, UpsertStepFollowUpRequest request)
Task<Result> DeleteStepFollowUpAsync(Guid agentId, Guid stepId)
Knowledge Blocks:
Task<Result<List<KnowledgeBlockResponse>>> GetKnowledgeBlocksAsync(Guid agentId)
Task<Result<KnowledgeBlockResponse>> GetKnowledgeBlockByIdAsync(Guid agentId, Guid id)
Task<Result<KnowledgeBlockResponse>> CreateKnowledgeBlockAsync(Guid agentId, CreateKnowledgeBlockRequest request)
Task<Result<KnowledgeBlockResponse>> UpdateKnowledgeBlockAsync(Guid agentId, Guid id, UpdateKnowledgeBlockRequest request)
Task<Result> DeleteKnowledgeBlockAsync(Guid agentId, Guid id)
Task<Result<List<KnowledgeBlockResponse>>> ReorderKnowledgeBlocksAsync(Guid agentId, List<Guid> orderedIds)
FAQ:
Task<Result<List<FaqItemResponse>>> GetFaqItemsAsync(Guid agentId)
Task<Result<FaqItemResponse>> GetFaqItemByIdAsync(Guid agentId, Guid id)
Task<Result<FaqItemResponse>> CreateFaqItemAsync(Guid agentId, CreateFaqItemRequest request)
Task<Result<FaqItemResponse>> UpdateFaqItemAsync(Guid agentId, Guid id, UpdateFaqItemRequest request)
Task<Result> DeleteFaqItemAsync(Guid agentId, Guid id)
LLM:
Instâncias:
Task<Result<List<InstanceListResponse>>> GetInstancesAsync(bool includeArchived = false)
Task<Result<InstanceDetailResponse>> GetInstanceByIdAsync(Guid id)
Task<Result<CreateInstanceResponse>> CreateInstanceAsync(CreateInstanceRequest request)
Task<Result<UpdateInstanceResponse>> UpdateInstanceAsync(Guid id, UpdateInstanceRequest request)
Task<Result> DeleteInstanceAsync(Guid id)
Task<Result> RestoreInstanceAsync(Guid id)
Task<Result> HardDeleteInstanceAsync(Guid id, string confirmationName)
Task<Result<ConnectResponse>> ConnectInstanceAsync(Guid id)
Task<Result<ConnectionStatusResponse>> GetInstanceStatusAsync(Guid id)
Task<Result> DisconnectInstanceAsync(Guid id)
Whitelist:
Task<Result<List<WhitelistEntryResponse>>> GetInstanceWhitelistAsync(Guid instanceId)
Task<Result<WhitelistEntryResponse>> CreateInstanceWhitelistEntryAsync(Guid instanceId, CreateWhitelistEntryRequest request)
Task<Result<WhitelistEntryResponse>> UpdateInstanceWhitelistEntryAsync(Guid instanceId, Guid id, UpdateWhitelistEntryRequest request)
Task<Result> DeleteInstanceWhitelistEntryAsync(Guid instanceId, Guid id)
Blacklist:
Task<Result<List<BlacklistEntryResponse>>> GetInstanceBlacklistAsync(Guid instanceId)
Task<Result<BlacklistEntryResponse>> CreateInstanceBlacklistEntryAsync(Guid instanceId, CreateBlacklistEntryRequest request)
Task<Result<BlacklistEntryResponse>> UpdateInstanceBlacklistEntryAsync(Guid instanceId, Guid id, UpdateBlacklistEntryRequest request)
Task<Result> DeleteInstanceBlacklistEntryAsync(Guid instanceId, Guid id)
Conversas:
Task<Result<ConversationListResponse>> GetConversationsByInstanceAsync(Guid instanceId, int page, int pageSize, string? status, string? contactPhone, string? search, DateTime? fromDate, DateTime? toDate)
Task<Result<ConversationDetailResponse>> GetConversationByIdAsync(Guid conversationId)
Task<Result<MessagePageResponse>> GetConversationMessagesAsync(Guid conversationId, int pageSize, DateTime? before, Guid? beforeId)
Task<Result<SendMessageResponse>> SendMessageAsync(Guid conversationId, string content, Guid? messageId)
Task<Result<SendMessageResponse>> SendMessageWithMediaAsync(Guid conversationId, string? content, List<AttachmentDto>? attachments, Guid? messageId)
Task<Result<SendMessageResponse>> SendVoiceMessageAsync(Guid conversationId, string base64Audio, string mimeType)
Task<Result<TransferConversationResponse>> TransferConversationAsync(Guid conversationId, string targetMode, bool triggerImmediateResponse)
Task<Result<ReactToMessageResponse>> ReactToMessageAsync(Guid conversationId, Guid messageId, string emoji)
Task<Result<ReactToMessageResponse>> RemoveReactionAsync(Guid conversationId, Guid messageId)
Task<Result<ResolveConversationResponse>> ResolveConversationAsync(Guid conversationId, string? summary)
Task<Result> MarkConversationAsReadAsync(Guid conversationId)
Task<Result<StartConversationResponse>> StartConversationAsync(Guid instanceId, string contactPhone, string? contactName)
Tenants:
Task<Result<List<TenantListResponse>>> GetTenantsAsync()
Task<Result<TenantListResponse>> GetTenantByIdAsync(Guid id)
Task<Result<CreateTenantResponse>> CreateTenantAsync(CreateTenantRequest request)
Task<Result<UpdateTenantResponse>> UpdateTenantAsync(Guid id, UpdateTenantRequest request)
Task<Result<SubscriptionHistoryResponse>> GetSubscriptionHistoryAsync(Guid tenantId)
Task<Result<SubscriptionDetailsResponse>> GetTenantSubscriptionDetailsAsync(Guid tenantId)
Admin Tenants (Busca):
LLM Usage (Admin):
Task<Result<LlmUsageSummaryResponse>> GetLlmUsageSummaryAsync(DateTime startDate, DateTime endDate, string? provider, string? operationType, Guid? tenantId)
Task<Result<List<LlmUsageSummaryItem>>> GetLlmUsageByModelAsync(DateTime startDate, DateTime endDate, string? provider, string? operationType, Guid? tenantId)
Task<Result<List<LlmUsageByOperationType>>> GetLlmUsageByOperationAsync(DateTime startDate, DateTime endDate, string? provider, string? operationType, Guid? tenantId)
Task<Result<PagedResult<LlmUsageByTenant>>> GetLlmUsageByTenantAsync(DateTime startDate, DateTime endDate, string? provider, string? operationType, Guid? tenantId, int page, int pageSize)
Task<Result<MarginByTenantResponse>> GetMarginByTenantAsync(DateTime startDate, DateTime endDate, string? provider, string? operationType, Guid? tenantId, int page, int pageSize)
Dashboard:
Task<Result<KpiOverviewResponse>> GetKpiOverviewAsync(DateTime startDate, DateTime endDate, Guid? agentId)
Task<Result<ConversationsOverTimeResponse>> GetConversationsOverTimeAsync(DateTime startDate, DateTime endDate, string granularity, Guid? agentId)
Task<Result<StepFunnelResponse>> GetStepFunnelAsync(DateTime startDate, DateTime endDate, Guid? agentId)
Task<Result<PeakHoursResponse>> GetPeakHoursAsync(DateTime startDate, DateTime endDate, Guid? agentId)
Task<Result<FollowUpEffectivenessResponse>> GetFollowUpEffectivenessAsync(DateTime startDate, DateTime endDate, Guid? agentId)
Onboarding:
Task<Result<OnboardingStatusResponse>> GetOnboardingStatusAsync()
Task<Result> CompleteOnboardingAsync()
Task<Result> DismissOnboardingAsync()
Subscription:
Task<Result<CreditBalanceResponse>> GetCreditBalanceAsync()
Task<Result<SubscriptionDetailsResponse>> GetSubscriptionDetailsAsync()
Uso¶
public partial class AgentList
{
[Inject] private ApiClient ApiClient { get; set; } = null!;
private async Task Load()
{
var result = await ApiClient.GetAgentsAsync();
if (result.IsSuccess)
{
_agents = result.Value;
}
}
}
AuthService¶
Gerenciamento de autenticação e sessão do usuário.
Arquivo: Services/AuthService.cs
Responsabilidades¶
- Login e logout
- Armazenamento de token JWT no LocalStorage
- Verificação de roles (SuperAdmin, Admin)
- Seleção de tenant para SuperAdmin
Dados Armazenados (LocalStorage)¶
| Chave | Conteudo |
|---|---|
authToken |
JWT token |
userName |
Nome do usuario |
userRole |
Role (SuperAdmin, Admin, User) |
selectedTenantId |
Tenant selecionado (SuperAdmin) |
selectedTenantName |
Nome do tenant selecionado |
onboardingChecked |
Flag de cache do onboarding (limpo no logout) |
welcomeCardDismissed |
Flag de dismiss do welcome card (limpo no logout) |
Métodos¶
// Autenticação
Task<Result> LoginAsync(string email, string password)
Task LogoutAsync()
Task<string?> GetTokenAsync()
Task<string?> GetUserNameAsync()
Task<string?> GetRoleAsync()
// Verificação de roles
Task<bool> IsSuperAdminAsync()
Task<bool> IsAdminAsync()
// Seleção de tenant (SuperAdmin)
Task SetSelectedTenantAsync(Guid tenantId, string tenantName)
Task<Guid?> GetSelectedTenantIdAsync()
Task<string?> GetSelectedTenantNameAsync()
Task<bool> HasTenantSelectedAsync()
// Eventos
event Action? OnTenantChanged
Uso¶
// No Login
var result = await AuthService.LoginAsync(email, password);
if (result.IsSuccess)
{
Navigation.NavigateTo("/");
}
// No Logout
await AuthService.LogoutAsync();
Navigation.NavigateTo("/login");
// Verificação de role
if (await AuthService.IsSuperAdminAsync())
{
// Mostrar menu de tenants
}
AuthorizationMessageHandler¶
DelegatingHandler que injeta JWT e tenant em todas as requisições HTTP.
Arquivo: Services/AuthorizationMessageHandler.cs
Comportamento¶
- Adiciona
Authorization: Bearer {token}em todas as requests - Adiciona
X-Tenant-Idse SuperAdmin tem tenant selecionado - Redireciona para
/loginem caso de401 Unauthorized - Limpa LocalStorage no logout automático
Registro¶
// Program.cs
services.AddTransient<AuthorizationMessageHandler>();
services.AddHttpClient<ApiClient>(client =>
{
client.BaseAddress = new Uri(apiBaseUrl);
})
.AddHttpMessageHandler<AuthorizationMessageHandler>();
SignalRService¶
Conexão WebSocket para atualizações em tempo real.
Arquivo: Services/SignalRService.cs
Eventos¶
event Action<Guid, MessageNotification>? OnNewMessage
event Action<Guid, Guid, string, DateTime?>? OnMessageStatusUpdated
event Action<ConversationNotification>? OnConversationUpdated
event Action<ConversationNotification>? OnConversationCreated
event Action<Guid, string?>? OnProfilePictureUpdated
event Action<Guid, ReactionNotification>? OnMessageReaction
event Action<int, string>? OnCreditBalanceUpdated
event Action<HubConnectionState>? OnConnectionStateChanged
Métodos¶
Task ConnectAsync()
Task DisconnectAsync()
Task JoinConversationAsync(Guid conversationId)
Task LeaveConversationAsync(Guid conversationId)
Task JoinTenantAsync(Guid tenantId)
Task LeaveTenantAsync(Guid tenantId)
Propriedades¶
Reconexão Automática¶
.WithAutomaticReconnect(new[]
{
TimeSpan.Zero, // Imediato
TimeSpan.FromSeconds(2),
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(10),
TimeSpan.FromSeconds(30)
})
DTOs¶
record MessageNotification(
Guid Id, string Role, string Type, string Content,
string? MediaUrl, string? MediaMimeType, string? MediaFileName,
string Status, DateTime CreatedAt);
record ConversationNotification(
Guid Id, Guid InstanceId, string ContactPhone, string? ContactName,
string? ContactProfilePictureUrl, string Status, string Mode,
DateTime LastMessageAt, string? LastMessage, string? LastMessageRole,
string? LastMessageStatus, int MessageCount, int UnreadCount);
record ReactionNotification(
Guid MessageId, string Emoji, string SenderPhone,
bool FromMe, DateTime CreatedAt);
Uso¶
public partial class ConversationChat : IDisposable
{
[Inject] private SignalRService SignalR { get; set; } = null!;
protected override async Task OnInitializedAsync()
{
SignalR.OnNewMessage += HandleNewMessage;
await SignalR.ConnectAsync();
await SignalR.JoinConversationAsync(ConversationId);
}
private void HandleNewMessage(Guid conversationId, MessageNotification message)
{
if (conversationId == ConversationId)
{
_messages.Add(message);
InvokeAsync(StateHasChanged);
}
}
public void Dispose()
{
SignalR.OnNewMessage -= HandleNewMessage;
}
}
SnackbarExtensions¶
Extensions para exibir feedback de erros via Snackbar.
Arquivo: Services/SnackbarExtensions.cs
Métodos¶
// Retorna true se erro foi exibido
bool ShowIfError(this ISnackbar snackbar, Result result)
bool ShowIfError<T>(this ISnackbar snackbar, Result<T> result)
Uso¶
var result = await ApiClient.DeleteAgentAsync(id);
if (Snackbar.ShowIfError(result)) return; // Early return se erro
Snackbar.Add("Agente excluído!", Severity.Success);
await Load();
DialogHelper¶
Padroniza abertura de dialogs de formulário. Sempre usar DialogHelper ao invés de criar DialogOptions manualmente.
Arquivo: Services/DialogHelper.cs
Métodos¶
// Retorna true se dialog foi submetido (não cancelado)
Task<bool> ShowFormAsync<TDialog>(
string? title = null,
DialogParameters<TDialog>? parameters = null,
MaxWidth maxWidth = MaxWidth.Small,
bool closeButton = false)
// Retorna dados do dialog (ou null se cancelado)
Task<TResult?> ShowFormAsync<TDialog, TResult>(
string? title = null,
DialogParameters<TDialog>? parameters = null,
MaxWidth maxWidth = MaxWidth.Small,
bool closeButton = false)
// Retorna DialogResult para controle total
Task<DialogResult?> ShowAsync<TDialog>(
string? title = null,
DialogParameters<TDialog>? parameters = null,
MaxWidth maxWidth = MaxWidth.Small,
bool closeButton = false,
bool allowClose = false) // habilita BackdropClick e EscapeKey
Uso¶
[Inject] private DialogHelper Dialog { get; set; } = null!;
// Simples - apenas verifica se não cancelou
if (await Dialog.ShowFormAsync<AgentCreateDialog>())
await LoadAgents();
// Com parâmetros
var parameters = new DialogParameters<KnowledgeBlockDialog>
{
{ x => x.AgentId, AgentId },
{ x => x.Block, null }
};
if (await Dialog.ShowFormAsync<KnowledgeBlockDialog>("Novo Bloco", parameters))
await LoadBlocks();
// Tamanho diferente
await Dialog.ShowFormAsync<MeuDialog>(maxWidth: MaxWidth.Medium);
// Retorna dados do dialog
var result = await Dialog.ShowFormAsync<StartConversationDialog, StartConversationDialogResult>(
"Nova Conversa", parameters, closeButton: true);
if (result != null)
Navigation.NavigateTo($"/conversations/{result.ConversationId}");
// Controle total (ex: para navegação com ID criado)
var result = await Dialog.ShowAsync<InstanceCreateDialog>();
if (result is { Canceled: false, Data: Guid createdId })
Navigation.NavigateTo($"/instances/{createdId}?connect=true");
Nota: Para
ShowMessageBox(confirmações simples), ainda usarIDialogServicediretamente.
ConfirmDeleteHelper¶
Padroniza confirmação de exclusão com feedback.
Arquivo: Services/ConfirmDeleteHelper.cs
Métodos¶
// Arquiva com confirmação — retorna true se arquivado com sucesso
Task<bool> ConfirmAndArchive(string itemName, Func<Task<Result>> archiveAction)
// Exclui com confirmação — retorna true se excluído com sucesso
Task<bool> ConfirmAndDelete(string itemName, Func<Task<Result>> deleteAction)
// Exclui permanentemente com type-to-confirm — retorna true se excluído
Task<bool> ConfirmAndDeletePermanently(
string itemName, string dataLossWarning, Func<Task<Result>> deleteAction)
Uso¶
// Arquivamento (soft delete)
var archived = await ConfirmDeleteHelper.ConfirmAndArchive(
_agent.Name,
() => ApiClient.DeleteAgentAsync(Id));
if (archived) await Load();
// Exclusão permanente (type-to-confirm)
var deleted = await ConfirmDeleteHelper.ConfirmAndDeletePermanently(
agent.Name,
"Todos os dados do agente serão removidos permanentemente.",
() => ApiClient.HardDeleteAgentAsync(agent.Id, agent.Name));
if (deleted) await Load();
StatusFormatService¶
Utilitários estáticos para formatação de status e datas.
Arquivo: Services/StatusFormatService.cs
Métodos¶
// Status de conexão
static Color GetStatusColor(string? status) // connected → Success
static string GetStatusLabel(string? status) // connected → "Conectado"
// Status de conversa
static Color GetConversationStatusColor(string? status)
static string GetConversationStatusLabel(string? status)
// Formatação de data
static string FormatDateTime(DateTime dateTime, string format = "dd/MM/yyyy HH:mm")
static string FormatLocalDateTime(DateTime utcDateTime, string format = "dd/MM/yyyy HH:mm")
// Stage da conversa (aguardando cliente/atendimento)
static string GetConversationStage(string status, string? lastMessageRole)
static Color GetConversationStageColor(string status, string? lastMessageRole)
Mapeamento de Status¶
Conexão:
| Status | Label | Cor |
|--------|-------|-----|
| connected/open | "Conectado" | Success |
| connecting | "Conectando" | Warning |
| disconnected/close | "Desconectado" | Default |
Conversa:
| Status | Label | Cor |
|--------|-------|-----|
| Active | "Ativa" | Success |
| Escalated | "Escalada" | Warning |
| Resolved | "Resolvida" | Default |
Stage (Conversa Ativa):
| LastMessageRole | Stage | Cor |
|-----------------|-------|-----|
| User | "Aguardando Atendimento" | Error |
| Assistant | "Aguardando Cliente" | Success |
| null | "Nova Conversa" | Info |
Uso¶
<MudChip Color="@StatusFormatService.GetStatusColor(_instance.Status)">
@StatusFormatService.GetStatusLabel(_instance.Status)
</MudChip>
WhatsAppFormatter¶
Converte formatação WhatsApp em HTML para exibição no frontend. Classe estática com regex compilados.
Arquivo: Helpers/WhatsAppFormatter.cs
Formatação Suportada¶
| HTML | Exemplo | |
|---|---|---|
*texto* |
<b>texto</b> |
texto |
_texto_ |
<i>texto</i> |
texto |
~texto~ |
<s>texto</s> |
~~texto~~ |
`texto` |
<code>texto</code> |
texto |
\n |
<br> |
Quebra de linha |
Segurança¶
- Aplica
WebUtility.HtmlEncodeantes das substituições regex para prevenir XSS - Retorna
MarkupStringpara renderização segura no Blazor
Uso¶
SubscriptionStateService¶
Cache client-side para dados de subscription, evitando chamadas redundantes à API.
Arquivo: Services/SubscriptionStateService.cs
Responsabilidades¶
- Cache in-memory de
SubscriptionDetailsResponsecom TTL de 5 minutos - Thread-safe via
SemaphoreSlimcom double-checked locking - Atualização real-time via
SignalRService.OnCreditBalanceUpdated - Notificação de componentes via evento
OnChanged
Propriedades e Eventos¶
int CreditBalance { get; } // Saldo atual (atualizado via SignalR)
string? Status { get; } // Status atual (atualizado via SignalR)
event Action? OnChanged; // Notifica componentes quando dados mudam
Métodos¶
// Retorna dados cacheados (ou busca da API se expirado/vazio)
Task<SubscriptionDetailsResponse?> GetDetailsAsync()
// Invalida cache (força re-fetch na próxima chamada)
void Invalidate()
Comportamento¶
GetDetailsAsync()verifica TTL (5 min) — se válido, retorna cache- Se expirado, busca via
ApiClient.GetSubscriptionDetailsAsync()com lock SignalRService.OnCreditBalanceUpdatedatualizaCreditBalance/Statuse o cache viawithexpressionOnChangedé disparado em qualquer atualização (SignalR ou invalidação)
Uso¶
[Inject] private SubscriptionStateService SubscriptionState { get; set; } = null!;
// Buscar dados (com cache)
var details = await SubscriptionState.GetDetailsAsync();
// Paralelo com outras chamadas
var agentsTask = Api.GetAgentsAsync();
var subTask = SubscriptionState.GetDetailsAsync();
await Task.WhenAll(agentsTask, subTask);
// Escutar mudanças
SubscriptionState.OnChanged += () => InvokeAsync(StateHasChanged);
Usado por¶
MainLayout— exibe saldo de créditos no headerAgentList— verifica limite de agentesInstanceList— verifica limite de instânciasSubscriptionDetails— exibe dados completos
Lifecycle¶
- Registrado como
Scoped(cada tab/circuito Blazor WASM tem o próprio) - Implementa
IDisposable(unsubscribe do SignalR, dispose do SemaphoreSlim)
PlaygroundStorageService¶
Persistência de conversas do Playground em localStorage via JS interop.
Arquivo: Services/PlaygroundStorageService.cs
Responsabilidades¶
- Armazenar e recuperar conversas e mensagens do Playground no localStorage
- Download de arquivos (exportação de chat em JSON)
- Scroll programático para elementos
- Remoção automática de dados base64 de mídia ao salvar (economia de espaço no localStorage)
Métodos¶
// Conversas (múltiplas por agente)
Task<List<PlaygroundConversation>> GetConversationsAsync(Guid agentId)
Task SaveConversationAsync(Guid agentId, PlaygroundConversation conversation)
Task DeleteConversationAsync(Guid agentId, string conversationId)
// Mensagens (API simplificada)
Task<List<PlaygroundMessage>> GetMessagesAsync(Guid agentId)
Task SaveMessagesAsync(Guid agentId, List<PlaygroundMessage> messages)
// Utilitários
Task DownloadFileAsync(string filename, string content, string contentType = "application/json")
Task ScrollToElementAsync(ElementReference element)
DTOs¶
record PlaygroundConversation(
string Id,
string Title,
DateTime CreatedAt,
DateTime UpdatedAt,
List<PlaygroundMessage> Messages);
record PlaygroundMessage(
string Role,
string Content,
string Type = "text",
string? MediaBase64 = null,
string? MediaMimeType = null,
string? MediaFileName = null,
int InputTokens = 0,
int OutputTokens = 0,
string? ExtractedText = null);
Chaves no localStorage¶
| Chave | Conteudo |
|---|---|
playground_{agentId} |
Lista de conversas com mensagens |
playground_messages_{agentId} |
Lista simplificada de mensagens |
Comportamento¶
- Carrega script JS (
js/playground.js) sob demanda na primeira chamada SaveMessagesAsyncremove dados base64 de mídia antes de salvar (evita estourar quota do localStorage)SaveConversationAsyncatualizaUpdatedAtautomaticamente ao re-salvar conversa existente
Uso¶
[Inject] private PlaygroundStorageService Storage { get; set; } = null!;
// Carregar mensagens
var messages = await Storage.GetMessagesAsync(agentId);
// Salvar mensagens
await Storage.SaveMessagesAsync(agentId, _messages);
// Exportar chat
var json = JsonSerializer.Serialize(_messages);
await Storage.DownloadFileAsync("chat.json", json);
Registro de Services¶
// Program.cs
services.AddBlazoredLocalStorage();
services.AddScoped<AuthService>();
services.AddScoped<SignalRService>();
services.AddScoped<DialogHelper>();
services.AddScoped<ConfirmDeleteHelper>();
services.AddScoped<SubscriptionStateService>();
services.AddScoped<PlaygroundStorageService>();
services.AddTransient<AuthorizationMessageHandler>();
services.AddHttpClient<ApiClient>(client =>
{
client.BaseAddress = new Uri(apiBaseUrl);
})
.AddHttpMessageHandler<AuthorizationMessageHandler>();
Padrões de Uso¶
Carregamento de Dados¶
private bool _loading = true;
private Result<List<AgentListResponse>>? _result;
protected override async Task OnInitializedAsync() => await Load();
private async Task Load()
{
_loading = true;
_result = await ApiClient.GetAgentsAsync();
_loading = false;
}
Ação com Feedback¶
private async Task Save()
{
_saving = true;
var result = await ApiClient.CreateAgentAsync(_request);
_saving = false;
if (Snackbar.ShowIfError(result)) return;
Snackbar.Add("Agente criado!", Severity.Success);
Navigation.NavigateTo("/agents");
}