Pular para conteúdo

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> ou Result (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:

Task<Result<LoginResponse>> LoginAsync(string email, string password)

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:

Task<Result<AvailableModelsResponse>> GetAvailableModelsAsync()

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):

Task<Result<List<TenantSearchItem>>> SearchTenantsAsync(string query)

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

  1. Adiciona Authorization: Bearer {token} em todas as requests
  2. Adiciona X-Tenant-Id se SuperAdmin tem tenant selecionado
  3. Redireciona para /login em caso de 401 Unauthorized
  4. 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

HubConnectionState State { get; }
bool IsConnected { get; }

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 usar IDialogService diretamente.


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

WhatsApp 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.HtmlEncode antes das substituições regex para prevenir XSS
  • Retorna MarkupString para renderização segura no Blazor

Uso

@((MarkupString)WhatsAppFormatter.FormatAsHtml(msg.Content).ToString())

SubscriptionStateService

Cache client-side para dados de subscription, evitando chamadas redundantes à API.

Arquivo: Services/SubscriptionStateService.cs

Responsabilidades

  • Cache in-memory de SubscriptionDetailsResponse com TTL de 5 minutos
  • Thread-safe via SemaphoreSlim com 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

  1. GetDetailsAsync() verifica TTL (5 min) — se válido, retorna cache
  2. Se expirado, busca via ApiClient.GetSubscriptionDetailsAsync() com lock
  3. SignalRService.OnCreditBalanceUpdated atualiza CreditBalance/Status e o cache via with expression
  4. OnChanged é 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 header
  • AgentList — verifica limite de agentes
  • InstanceList — verifica limite de instâncias
  • SubscriptionDetails — 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
  • SaveMessagesAsync remove dados base64 de mídia antes de salvar (evita estourar quota do localStorage)
  • SaveConversationAsync atualiza UpdatedAt automaticamente 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");
}

Exclusão com Confirmação

private async Task Delete()
{
    var deleted = await ConfirmDeleteHelper.ConfirmAndDelete(
        _item.Name,
        () => ApiClient.DeleteItemAsync(_item.Id));

    if (deleted) await Load();
}