mirror of
https://github.com/domfelipe/mika-agent-assist.git
synced 2026-08-07 15:56:50 +00:00
Changes
Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com>
This commit is contained in:
parent
75397543e8
commit
bebe095d6e
4 changed files with 188 additions and 2 deletions
|
|
@ -7,8 +7,14 @@ import { useAuth } from "@/hooks/use-auth";
|
||||||
export interface AgentInstance {
|
export interface AgentInstance {
|
||||||
id: string;
|
id: string;
|
||||||
user_id: string;
|
user_id: string;
|
||||||
|
uuid_tenant: string;
|
||||||
status: string;
|
status: string;
|
||||||
telegram_bot_username: string | null;
|
telegram_bot_username: string | null;
|
||||||
|
telegram_webhook_configured: boolean;
|
||||||
|
telegram_token_invalid: boolean;
|
||||||
|
telegram_first_message_received_at: string | null;
|
||||||
|
telegram_connected_at: string | null;
|
||||||
|
telegram_onboarding_completed: boolean;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -20,11 +26,13 @@ export function useAgentInstance() {
|
||||||
queryFn: async (): Promise<AgentInstance | null> => {
|
queryFn: async (): Promise<AgentInstance | null> => {
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from("agent_instances")
|
.from("agent_instances")
|
||||||
.select("id, user_id, status, telegram_bot_username, created_at")
|
.select(
|
||||||
|
"id, user_id, uuid_tenant, status, telegram_bot_username, telegram_webhook_configured, telegram_token_invalid, telegram_first_message_received_at, telegram_connected_at, telegram_onboarding_completed, created_at",
|
||||||
|
)
|
||||||
.eq("user_id", user!.id)
|
.eq("user_id", user!.id)
|
||||||
.maybeSingle();
|
.maybeSingle();
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
return data;
|
return data as AgentInstance | null;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
103
src/hooks/use-telegram-first-message.ts
Normal file
103
src/hooks/use-telegram-first-message.ts
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { supabase } from "@/integrations/supabase/client";
|
||||||
|
|
||||||
|
interface FirstMessage {
|
||||||
|
id: string;
|
||||||
|
created_at: string;
|
||||||
|
message_text: string | null;
|
||||||
|
message_type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aguarda a primeira mensagem incoming do Telegram para o agent_instance dado.
|
||||||
|
* - Subscribe via Realtime com filter explícito.
|
||||||
|
* - Fallback de polling a cada 5s caso o Realtime não conecte em 10s.
|
||||||
|
* - Considera apenas mensagens com created_at >= since (telegram_connected_at).
|
||||||
|
*/
|
||||||
|
export function useTelegramFirstMessage(opts: {
|
||||||
|
agentInstanceId: string | null | undefined;
|
||||||
|
since: string | null | undefined;
|
||||||
|
enabled: boolean;
|
||||||
|
}): { received: FirstMessage | null; reset: () => void } {
|
||||||
|
const { agentInstanceId, since, enabled } = opts;
|
||||||
|
const [received, setReceived] = useState<FirstMessage | null>(null);
|
||||||
|
const realtimeConnected = useRef(false);
|
||||||
|
const pollingRef = useRef<number | null>(null);
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
setReceived(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!enabled || !agentInstanceId) return;
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
realtimeConnected.current = false;
|
||||||
|
|
||||||
|
const channel = supabase
|
||||||
|
.channel(`telegram-messages-${agentInstanceId}`)
|
||||||
|
.on(
|
||||||
|
"postgres_changes",
|
||||||
|
{
|
||||||
|
event: "INSERT",
|
||||||
|
schema: "public",
|
||||||
|
table: "telegram_messages_log",
|
||||||
|
filter: `agent_instance_id=eq.${agentInstanceId}`,
|
||||||
|
},
|
||||||
|
(payload) => {
|
||||||
|
// deno-lint-ignore no-explicit-any
|
||||||
|
const row = payload.new as any;
|
||||||
|
if (row?.direction !== "incoming") return;
|
||||||
|
if (since && new Date(row.created_at) < new Date(since)) return;
|
||||||
|
if (cancelled) return;
|
||||||
|
setReceived({
|
||||||
|
id: row.id,
|
||||||
|
created_at: row.created_at,
|
||||||
|
message_text: row.message_text ?? null,
|
||||||
|
message_type: row.message_type ?? "text",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.subscribe((status) => {
|
||||||
|
if (status === "SUBSCRIBED") {
|
||||||
|
realtimeConnected.current = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fallback de polling: começa em 10s caso Realtime ainda não tenha conectado
|
||||||
|
const fallbackTimer = window.setTimeout(() => {
|
||||||
|
if (realtimeConnected.current || cancelled) return;
|
||||||
|
pollingRef.current = window.setInterval(async () => {
|
||||||
|
if (cancelled) return;
|
||||||
|
let q = supabase
|
||||||
|
.from("telegram_messages_log")
|
||||||
|
.select("id, created_at, message_text, message_type, direction")
|
||||||
|
.eq("agent_instance_id", agentInstanceId)
|
||||||
|
.eq("direction", "incoming")
|
||||||
|
.order("created_at", { ascending: false })
|
||||||
|
.limit(1);
|
||||||
|
if (since) q = q.gte("created_at", since);
|
||||||
|
const { data } = await q.maybeSingle();
|
||||||
|
if (data && !cancelled) {
|
||||||
|
setReceived({
|
||||||
|
id: data.id as string,
|
||||||
|
created_at: data.created_at as string,
|
||||||
|
message_text: (data.message_text as string | null) ?? null,
|
||||||
|
message_type: (data.message_type as string) ?? "text",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
}, 10_000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
window.clearTimeout(fallbackTimer);
|
||||||
|
if (pollingRef.current) window.clearInterval(pollingRef.current);
|
||||||
|
supabase.removeChannel(channel);
|
||||||
|
};
|
||||||
|
}, [agentInstanceId, since, enabled]);
|
||||||
|
|
||||||
|
return { received, reset };
|
||||||
|
}
|
||||||
50
src/lib/invoke-function.ts
Normal file
50
src/lib/invoke-function.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { supabase } from "@/integrations/supabase/client";
|
||||||
|
|
||||||
|
interface InvokeResult<T> {
|
||||||
|
data: T | null;
|
||||||
|
error: { message: string; status?: number } | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper para supabase.functions.invoke que normaliza erros de função
|
||||||
|
* (FunctionsHttpError vem com Response no .context que precisa ser lido).
|
||||||
|
*/
|
||||||
|
export async function invokeFunction<T = unknown>(
|
||||||
|
name: string,
|
||||||
|
body?: Record<string, unknown>,
|
||||||
|
): Promise<InvokeResult<T>> {
|
||||||
|
try {
|
||||||
|
const { data, error } = await supabase.functions.invoke<T>(name, {
|
||||||
|
body: body ?? {},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
// Tenta extrair mensagem do response real
|
||||||
|
let msg = error.message ?? "Erro inesperado";
|
||||||
|
let status: number | undefined;
|
||||||
|
// deno-lint-ignore no-explicit-any
|
||||||
|
const ctx = (error as any).context as Response | undefined;
|
||||||
|
if (ctx && typeof ctx.json === "function") {
|
||||||
|
try {
|
||||||
|
status = ctx.status;
|
||||||
|
const parsed = await ctx.json();
|
||||||
|
if (parsed?.error) msg = parsed.error;
|
||||||
|
} catch {
|
||||||
|
// ignora
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { data: null, error: { message: msg, status } };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { data: (data ?? null) as T | null, error: null };
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
data: null,
|
||||||
|
error: {
|
||||||
|
message: err instanceof Error ? err.message : "Erro inesperado",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/lib/telegram-username.ts
Normal file
25
src/lib/telegram-username.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitiza um primeiro nome para uso em sugestões de username do Telegram.
|
||||||
|
* - remove acentos via NFD
|
||||||
|
* - remove caracteres não-alfanuméricos
|
||||||
|
* - lowercase
|
||||||
|
*/
|
||||||
|
export function sanitizeForUsername(name: string): string {
|
||||||
|
return (name || "")
|
||||||
|
.normalize("NFD")
|
||||||
|
.replace(/[\u0300-\u036f]/g, "")
|
||||||
|
.replace(/[^a-zA-Z0-9]/g, "")
|
||||||
|
.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function suggestBotName(fullName: string | null | undefined): string {
|
||||||
|
const first = (fullName || "").trim().split(/\s+/)[0] || "Você";
|
||||||
|
return `Mika de ${first}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function suggestBotUsername(fullName: string | null | undefined): string {
|
||||||
|
const first = sanitizeForUsername((fullName || "").trim().split(/\s+/)[0] || "voce");
|
||||||
|
return `mika_${first || "voce"}_bot`;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue