- Avatar do paciente agora persiste após reload (adiciona timestamp para evitar cache) - Agendamento usa patient_id correto ao invés de user_id - Botão de download de PDF desbloqueado com logs detalhados
416 lines
12 KiB
TypeScript
416 lines
12 KiB
TypeScript
/**
|
|
* Cloudflare Workers function for chatbot API
|
|
* Proxies requests to Groq API using the secure API key from environment variables
|
|
* Provides role-specific assistance based on user type (médico, paciente, secretária)
|
|
*/
|
|
|
|
interface Env {
|
|
GROQ_API_KEY: string;
|
|
SUPABASE_URL: string;
|
|
SUPABASE_ANON_KEY: string;
|
|
}
|
|
|
|
interface ChatMessage {
|
|
role: "user" | "assistant" | "system";
|
|
content: string;
|
|
}
|
|
|
|
interface ChatRequest {
|
|
messages: ChatMessage[];
|
|
token?: string;
|
|
}
|
|
|
|
interface UserProfile {
|
|
id: string;
|
|
role: "medico" | "paciente" | "secretaria" | "admin";
|
|
nome?: string;
|
|
especialidade?: string;
|
|
}
|
|
|
|
async function getUserProfile(
|
|
token: string,
|
|
env: Env
|
|
): Promise<UserProfile | null> {
|
|
try {
|
|
const supabaseUrl =
|
|
env.SUPABASE_URL || "https://yuanqfswhberkoevtmfr.supabase.co";
|
|
|
|
// Get user from token
|
|
const userResponse = await fetch(`${supabaseUrl}/auth/v1/user`, {
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
apikey: env.SUPABASE_ANON_KEY,
|
|
},
|
|
});
|
|
|
|
if (!userResponse.ok) return null;
|
|
|
|
const user = await userResponse.json();
|
|
|
|
// Get user profile from usuarios table
|
|
const profileResponse = await fetch(
|
|
`${supabaseUrl}/rest/v1/usuarios?id=eq.${user.id}&select=id,role,nome,especialidade`,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
apikey: env.SUPABASE_ANON_KEY,
|
|
"Content-Type": "application/json",
|
|
},
|
|
}
|
|
);
|
|
|
|
if (!profileResponse.ok) return null;
|
|
|
|
const profiles = await profileResponse.json();
|
|
return profiles[0] || null;
|
|
} catch (error) {
|
|
console.error("Error fetching user profile:", error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function getRoleSpecificPrompt(profile: UserProfile | null): string {
|
|
if (!profile) {
|
|
return `Você é a Conni, a Assistente Virtual do MediConnect, uma plataforma de gestão médica.
|
|
|
|
SEU NOME: Conni - sempre se apresente como "Conni" quando perguntarem seu nome.
|
|
|
|
Suas responsabilidades:
|
|
- Responder dúvidas gerais sobre o sistema
|
|
- Explicar funcionalidades básicas
|
|
- Orientar sobre como fazer login
|
|
- Fornecer informações sobre agendamento de consultas
|
|
|
|
IMPORTANTE:
|
|
- NUNCA solicite ou processe dados sensíveis de pacientes (PHI)
|
|
- NUNCA forneça diagnósticos médicos
|
|
- Seja sempre educado, claro e objetivo
|
|
- Responda em português do Brasil`;
|
|
}
|
|
|
|
const baseRules = `
|
|
REGRAS IMPORTANTES:
|
|
- SEU NOME É CONNI - sempre se apresente como "Conni" quando perguntarem
|
|
- NUNCA solicite ou processe dados sensíveis de pacientes (PHI) em detalhes
|
|
- NUNCA forneça diagnósticos médicos
|
|
- Seja sempre educado, claro e objetivo
|
|
- Responda em português do Brasil
|
|
- Forneça informações práticas e orientações de uso do sistema`;
|
|
|
|
switch (profile.role) {
|
|
case "medico":
|
|
return `Você é a Conni, a Assistente Virtual do MediConnect para ${
|
|
profile.nome || "Médico"
|
|
}${profile.especialidade ? ` - ${profile.especialidade}` : ""}.
|
|
|
|
SEU NOME: Conni - sempre se apresente como "Conni" quando perguntarem seu nome.
|
|
|
|
FUNCIONALIDADES DISPONÍVEIS PARA MÉDICOS:
|
|
1. **Agenda e Consultas**:
|
|
- Visualizar agenda do dia/semana/mês
|
|
- Gerenciar disponibilidade de horários
|
|
- Confirmar ou reagendar consultas
|
|
- Adicionar exceções de horários (férias, folgas)
|
|
|
|
2. **Prontuários**:
|
|
- Acessar histórico completo de pacientes
|
|
- Adicionar evoluções e diagnósticos
|
|
- Registrar prescrições e exames
|
|
- Visualizar consultas anteriores
|
|
|
|
3. **Atendimentos**:
|
|
- Iniciar consulta do dia
|
|
- Registrar informações durante atendimento
|
|
- Gerar relatórios de atendimento
|
|
- Solicitar exames complementares
|
|
|
|
4. **Comunicação**:
|
|
- Sistema de mensagens com pacientes
|
|
- Enviar orientações pós-consulta
|
|
- Responder dúvidas gerais (não diagnósticos remotos)
|
|
|
|
5. **Relatórios e Estatísticas**:
|
|
- Visualizar número de atendimentos
|
|
- Consultar taxa de comparecimento
|
|
- Acessar métricas de desempenho
|
|
|
|
VOCÊ PODE AJUDAR O MÉDICO A:
|
|
- Explicar como usar cada funcionalidade
|
|
- Encontrar opções no painel médico
|
|
- Resolver problemas técnicos
|
|
- Otimizar o fluxo de trabalho
|
|
${baseRules}`;
|
|
|
|
case "paciente":
|
|
return `Você é a Conni, a Assistente Virtual do MediConnect para ${
|
|
profile.nome || "Paciente"
|
|
}.
|
|
|
|
SEU NOME: Conni - sempre se apresente como "Conni" quando perguntarem seu nome.
|
|
|
|
FUNCIONALIDADES DISPONÍVEIS PARA PACIENTES:
|
|
1. **Agendamento de Consultas**:
|
|
- Buscar médicos por especialidade
|
|
- Visualizar horários disponíveis
|
|
- Agendar nova consulta
|
|
- Reagendar ou cancelar consultas existentes
|
|
- Receber confirmações por SMS/email
|
|
|
|
2. **Minhas Consultas**:
|
|
- Ver consultas agendadas (próximas e histórico)
|
|
- Visualizar detalhes da consulta
|
|
- Informações do médico (especialidade, local)
|
|
- Status da consulta (confirmada, pendente, concluída)
|
|
|
|
3. **Histórico Médico**:
|
|
- Acessar prontuário pessoal
|
|
- Visualizar diagnósticos anteriores
|
|
- Consultar prescrições médicas
|
|
- Ver resultados de exames (se disponível)
|
|
|
|
4. **Comunicação**:
|
|
- Enviar mensagens para médicos
|
|
- Receber orientações pós-consulta
|
|
- Tirar dúvidas gerais (não substitui consulta)
|
|
|
|
5. **Perfil**:
|
|
- Atualizar dados pessoais
|
|
- Gerenciar informações de contato
|
|
- Configurar preferências de notificação
|
|
|
|
VOCÊ PODE AJUDAR O PACIENTE A:
|
|
- Agendar e gerenciar consultas
|
|
- Encontrar médicos e especialidades
|
|
- Navegar pelo sistema
|
|
- Entender como acessar informações médicas
|
|
- Resolver dúvidas sobre o uso da plataforma
|
|
${baseRules}
|
|
|
|
ATENÇÃO: Para dúvidas médicas específicas, oriente a agendar uma consulta.`;
|
|
|
|
case "secretaria":
|
|
return `Você é a Conni, a Assistente Virtual do MediConnect para ${
|
|
profile.nome || "Secretária"
|
|
}.
|
|
|
|
SEU NOME: Conni - sempre se apresente como "Conni" quando perguntarem seu nome.
|
|
|
|
FUNCIONALIDADES DISPONÍVEIS PARA SECRETÁRIAS:
|
|
1. **Gestão de Agenda**:
|
|
- Visualizar agenda de todos os médicos
|
|
- Agendar consultas para pacientes
|
|
- Confirmar, reagendar ou cancelar consultas
|
|
- Gerenciar lista de espera
|
|
- Bloquear horários para eventos especiais
|
|
|
|
2. **Cadastro de Pacientes**:
|
|
- Registrar novos pacientes
|
|
- Atualizar dados cadastrais
|
|
- Verificar histórico de consultas
|
|
- Gerenciar documentos e informações de contato
|
|
|
|
3. **Atendimento e Recepção**:
|
|
- Confirmar presença de pacientes
|
|
- Registrar chegadas
|
|
- Informar atrasos aos médicos
|
|
- Gerenciar fila de atendimento
|
|
|
|
4. **Comunicação**:
|
|
- Enviar lembretes de consultas (SMS/email)
|
|
- Confirmar agendamentos
|
|
- Notificar cancelamentos
|
|
- Comunicar mudanças de horário
|
|
|
|
5. **Relatórios Administrativos**:
|
|
- Gerar relatórios de agendamento
|
|
- Consultar taxa de ocupação
|
|
- Visualizar estatísticas de comparecimento
|
|
- Exportar dados para gestão
|
|
|
|
6. **Gestão de Médicos**:
|
|
- Visualizar disponibilidade dos médicos
|
|
- Coordenar exceções de agenda
|
|
- Gerenciar escalas e plantões
|
|
|
|
VOCÊ PODE AJUDAR A SECRETÁRIA A:
|
|
- Otimizar processos de agendamento
|
|
- Resolver conflitos de horários
|
|
- Encontrar funcionalidades no painel
|
|
- Gerenciar múltiplos médicos e pacientes
|
|
- Usar ferramentas de comunicação
|
|
- Gerar relatórios necessários
|
|
${baseRules}`;
|
|
|
|
case "admin":
|
|
return `Você é o Assistente Virtual do MediConnect para Administrador.
|
|
|
|
FUNCIONALIDADES ADMINISTRATIVAS:
|
|
- Gestão completa de usuários (médicos, pacientes, secretárias)
|
|
- Configurações do sistema
|
|
- Relatórios avançados e analytics
|
|
- Gerenciamento de permissões
|
|
- Monitoramento de performance
|
|
- Configurações de notificações
|
|
${baseRules}`;
|
|
|
|
default:
|
|
return `Você é o Assistente Virtual do MediConnect.
|
|
${baseRules}`;
|
|
}
|
|
}
|
|
|
|
export async function onRequest(context: { request: Request; env: Env }) {
|
|
// Handle CORS preflight
|
|
if (context.request.method === "OPTIONS") {
|
|
return new Response(null, {
|
|
headers: {
|
|
"Access-Control-Allow-Origin": "*",
|
|
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
},
|
|
});
|
|
}
|
|
|
|
if (context.request.method !== "POST") {
|
|
return new Response("Method not allowed", { status: 405 });
|
|
}
|
|
|
|
try {
|
|
const body: ChatRequest = await context.request.json();
|
|
|
|
// Validate Groq API key
|
|
if (!context.env.GROQ_API_KEY) {
|
|
console.error("GROQ_API_KEY not configured");
|
|
return new Response(
|
|
JSON.stringify({
|
|
reply:
|
|
"O serviço de chat está temporariamente indisponível. Por favor, contate o suporte.",
|
|
}),
|
|
{
|
|
status: 200,
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"Access-Control-Allow-Origin": "*",
|
|
},
|
|
}
|
|
);
|
|
}
|
|
|
|
// Get user profile from token
|
|
const authHeader = context.request.headers.get("Authorization");
|
|
const token = body.token || authHeader?.replace("Bearer ", "");
|
|
|
|
const userProfile = token ? await getUserProfile(token, context.env) : null;
|
|
|
|
// Get role-specific system prompt
|
|
const systemPrompt: ChatMessage = {
|
|
role: "system",
|
|
content: getRoleSpecificPrompt(userProfile),
|
|
};
|
|
|
|
// Prepare messages for OpenAI
|
|
const messages = [systemPrompt, ...body.messages];
|
|
|
|
// Call Groq API
|
|
const openaiResponse = await fetch(
|
|
"https://api.groq.com/openai/v1/chat/completions",
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: `Bearer ${context.env.GROQ_API_KEY}`,
|
|
},
|
|
body: JSON.stringify({
|
|
model: "llama-3.3-70b-versatile",
|
|
messages: messages,
|
|
max_tokens: 1000,
|
|
temperature: 0.7,
|
|
}),
|
|
}
|
|
);
|
|
|
|
if (!openaiResponse.ok) {
|
|
const errorText = await openaiResponse.text();
|
|
console.error("Groq API error:", openaiResponse.status, errorText);
|
|
|
|
// Handle specific error cases
|
|
if (openaiResponse.status === 401) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
reply:
|
|
"Erro de autenticação com o serviço de IA. Por favor, contate o administrador.",
|
|
}),
|
|
{
|
|
status: 200,
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"Access-Control-Allow-Origin": "*",
|
|
},
|
|
}
|
|
);
|
|
}
|
|
|
|
if (openaiResponse.status === 429) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
reply:
|
|
"O serviço está temporariamente sobrecarregado. Por favor, tente novamente em alguns instantes.",
|
|
}),
|
|
{
|
|
status: 200,
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"Access-Control-Allow-Origin": "*",
|
|
},
|
|
}
|
|
);
|
|
}
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
reply:
|
|
"Desculpe, ocorreu um erro ao processar sua mensagem. Tente novamente.",
|
|
}),
|
|
{
|
|
status: 200,
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"Access-Control-Allow-Origin": "*",
|
|
},
|
|
}
|
|
);
|
|
}
|
|
|
|
const data = await openaiResponse.json();
|
|
const reply =
|
|
data.choices[0]?.message?.content ||
|
|
"Desculpe, não consegui gerar uma resposta.";
|
|
|
|
return new Response(JSON.stringify({ reply }), {
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"Access-Control-Allow-Origin": "*",
|
|
},
|
|
});
|
|
} catch (error) {
|
|
console.error("Chat API error:", error);
|
|
const errorMessage =
|
|
error instanceof Error ? error.message : "Unknown error";
|
|
console.error("Error details:", errorMessage);
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
reply:
|
|
"Desculpe, ocorreu um erro ao processar sua mensagem. Tente novamente.",
|
|
}),
|
|
{
|
|
status: 200,
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"Access-Control-Allow-Origin": "*",
|
|
},
|
|
}
|
|
);
|
|
}
|
|
}
|