feat(captain-memory): add Contact Memory UI component + API client + i18n

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rodribm10 2026-04-19 01:47:56 -03:00
parent 8444209952
commit 2f7d8edd92
4 changed files with 211 additions and 4 deletions

View File

@ -0,0 +1,34 @@
/* global axios */
import ApiClient from '../ApiClient';
class ContactMemoriesAPI extends ApiClient {
constructor() {
super('memories', { accountScoped: true });
}
get url() {
return `${this.baseUrl()}/contacts/${this.contactId}/memories`;
}
list(contactId) {
this.contactId = contactId;
return axios.get(this.url);
}
update(contactId, id, payload) {
this.contactId = contactId;
return axios.patch(`${this.url}/${id}`, payload);
}
destroy(contactId, id) {
this.contactId = contactId;
return axios.delete(`${this.url}/${id}`);
}
forgetAll(contactId) {
this.contactId = contactId;
return axios.delete(this.url);
}
}
export default new ContactMemoriesAPI();

View File

@ -423,6 +423,14 @@
"SUCCESS_MESSAGE": "FAQ deleted successfully",
"ERROR_MESSAGE": "An error occurred while deleting FAQ, please try again."
}
},
"MEMORY": {
"LOADING": "Loading…",
"EMPTY": "No memories for this contact yet.",
"CONFIRM_DELETE": "Forget this memory?",
"CONFIRM_FORGET_ALL": "Forget ALL memories for this contact? This cannot be undone after 30 days.",
"FORGET": "Forget",
"FORGET_ALL": "Forget all"
}
},
"CAPTAIN_REPORTS": {
@ -437,8 +445,39 @@
"DASHBOARD": "Dashboard",
"INSIGHTS": "AI Insights",
"OPERATIONAL": "Operational",
"EXECUTIVE": "Executive",
"LANDING_PAGES": "Landing Pages"
},
"EXECUTIVE": {
"LOADING": "Loading executive digest...",
"NO_DATA": "No insights for the period. Run the weekly analysis to see data here.",
"TITLE": "Executive Digest",
"SUBTITLE": "Same report sent to Mattermost, with drill-down and filters.",
"DELIVER_BUTTON": "Send to Mattermost now",
"DELIVER_SUCCESS": "Digest queued. It will arrive in Mattermost shortly.",
"DELIVER_ERROR": "Failed to dispatch the digest. Check Rails log.",
"CONVERSATIONS": "Conversations",
"MESSAGES": "Messages",
"UNITS_ANALYZED": "Units analyzed",
"INSIGHTS_COUNT": "Insights generated",
"UNIT_TABLE": "Comparative by unit",
"COL_UNIT": "Unit",
"COL_CONVS": "Conversations",
"COL_DELTA": "vs previous week",
"COL_AI_RATE": "AI success rate",
"COL_FAILURES": "Failures",
"AI_FAILURES": "Where Angelina failed (click to see conversations)",
"OPPORTUNITIES": "Opportunities — what customers asked for",
"OPPORTUNITIES_HINT": "Click an opportunity to see real conversations where it was mentioned.",
"COMPLAINTS": "Recurring complaints",
"PRAISES": "Recurring praises",
"RECOMMENDATIONS": "AI Recommendations",
"DRILLDOWN_TITLE": "Related conversations",
"NO_CONVERSATIONS_FOUND": "No conversations found with those keywords in the period.",
"NO_CONVERSATIONS_HINT": "The insight description is an AI abstraction. If no keyword matches the literal conversation text, nothing is returned. Try clicking a more specific item.",
"SEARCH_TOKENS": "Keywords searched",
"OPEN_CONVERSATION": "Open in Chatwoot"
},
"LP": {
"LOADING": "Loading data...",
"NO_DATA": "No clicks recorded yet. Integrate the pixel on your landing page to see data here.",
@ -515,8 +554,23 @@
"FAILED": "Failed"
},
"OPERATIONAL": {
"COMING_SOON": "Coming soon",
"COMING_SOON_DESC": "Real-time operational data (reservations, Pix charges, etc.) will be available here soon."
"LOADING": "Loading operational data...",
"NO_DATA": "No operational data for the selected period.",
"CONVERSATIONS_SECTION": "Conversations",
"RESERVATIONS_SECTION": "Reservations",
"TOTAL": "Total",
"RESOLVED": "Resolved",
"OPEN": "Open",
"AVG_RESOLUTION": "Avg resolution time",
"RES_TOTAL": "Total reservations",
"RES_PAID": "Paid",
"RES_EXPIRED": "Expired",
"RES_REVENUE": "Paid revenue",
"BY_INBOX": "Volume by inbox",
"RESOLUTION_RATE_TOOLTIP": "Resolution rate",
"DAILY_DIST": "Daily distribution",
"HOURLY_DIST": "Hourly distribution",
"PEAK": "Peak"
},
"DASHBOARD": {
"TOTAL_CONVERSATIONS": "Analyzed conversations",

View File

@ -425,6 +425,14 @@
"SUCCESS_MESSAGE": "FAQ excluída com sucesso",
"ERROR_MESSAGE": "Ocorreu um erro ao excluir a FAQ, por favor tente novamente."
}
},
"MEMORY": {
"LOADING": "Carregando…",
"EMPTY": "Sem memórias para este contato ainda.",
"CONFIRM_DELETE": "Esquecer esta memória?",
"CONFIRM_FORGET_ALL": "Esquecer TODAS as memórias deste contato? Após 30 dias não é possível desfazer.",
"FORGET": "Esquecer",
"FORGET_ALL": "Esquecer tudo"
}
},
"CAPTAIN_REPORTS": {
@ -439,8 +447,39 @@
"DASHBOARD": "Dashboard",
"INSIGHTS": "Insights IA",
"OPERATIONAL": "Operacional",
"EXECUTIVE": "Executivo",
"LANDING_PAGES": "Landing Pages"
},
"EXECUTIVE": {
"LOADING": "Carregando digest executivo...",
"NO_DATA": "Sem insights gerados para o período. Rode a análise semanal pra ver os dados aqui.",
"TITLE": "Digest Executivo",
"SUBTITLE": "Mesmo relatório enviado ao Mattermost, com drill-down e filtros.",
"DELIVER_BUTTON": "Enviar ao Mattermost agora",
"DELIVER_SUCCESS": "Digest enfileirado. Vai chegar no Mattermost em instantes.",
"DELIVER_ERROR": "Falha ao disparar o digest. Veja o log do Rails.",
"CONVERSATIONS": "Conversas",
"MESSAGES": "Mensagens",
"UNITS_ANALYZED": "Unidades analisadas",
"INSIGHTS_COUNT": "Insights gerados",
"UNIT_TABLE": "Comparativo por unidade",
"COL_UNIT": "Unidade",
"COL_CONVS": "Conversas",
"COL_DELTA": "vs semana anterior",
"COL_AI_RATE": "Taxa de acerto IA",
"COL_FAILURES": "Falhas",
"AI_FAILURES": "Onde a Angelina errou (clique pra ver conversas)",
"OPPORTUNITIES": "Oportunidades — o que clientes pediram",
"OPPORTUNITIES_HINT": "Clique em uma oportunidade pra ver as conversas reais onde foi mencionada.",
"COMPLAINTS": "Reclamações recorrentes",
"PRAISES": "Elogios recorrentes",
"RECOMMENDATIONS": "Recomendações da IA",
"DRILLDOWN_TITLE": "Conversas relacionadas",
"NO_CONVERSATIONS_FOUND": "Nenhuma conversa encontrada com essas palavras no período.",
"NO_CONVERSATIONS_HINT": "A descrição do insight é uma abstração da IA. Se nenhuma palavra-chave bateu com o texto literal das conversas, nada é retornado. Tente clicar em outro item mais específico.",
"SEARCH_TOKENS": "Palavras buscadas",
"OPEN_CONVERSATION": "Abrir no Chatwoot"
},
"LP": {
"LOADING": "Carregando dados...",
"NO_DATA": "Nenhum clique registrado ainda. Integre o pixel na landing page para ver os dados aqui.",
@ -517,8 +556,23 @@
"FAILED": "Falhou"
},
"OPERATIONAL": {
"COMING_SOON": "Em breve",
"COMING_SOON_DESC": "Os dados operacionais em tempo real (reservas, cobranças Pix, etc.) estarão disponíveis aqui em breve."
"LOADING": "Carregando dados operacionais...",
"NO_DATA": "Sem dados operacionais para o período selecionado.",
"CONVERSATIONS_SECTION": "Conversas",
"RESERVATIONS_SECTION": "Reservas",
"TOTAL": "Total",
"RESOLVED": "Resolvidas",
"OPEN": "Em aberto",
"AVG_RESOLUTION": "Tempo médio de resolução",
"RES_TOTAL": "Total de reservas",
"RES_PAID": "Pagas",
"RES_EXPIRED": "Expiradas",
"RES_REVENUE": "Receita paga",
"BY_INBOX": "Volume por canal",
"RESOLUTION_RATE_TOOLTIP": "Taxa de resolução",
"DAILY_DIST": "Distribuição por dia",
"HOURLY_DIST": "Distribuição por hora",
"PEAK": "Pico"
},
"DASHBOARD": {
"TOTAL_CONVERSATIONS": "Conversas analisadas",

View File

@ -0,0 +1,65 @@
<script setup>
import { ref, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import ContactMemoriesAPI from 'dashboard/api/captain/contactMemories';
const props = defineProps({
contactId: { type: [String, Number], required: true },
});
const { t } = useI18n();
const memories = ref([]);
const loading = ref(true);
const error = ref(null);
async function load() {
loading.value = true;
error.value = null;
try {
const { data } = await ContactMemoriesAPI.list(props.contactId);
memories.value = data.data;
} catch (e) {
error.value = e.message;
} finally {
loading.value = false;
}
}
async function deleteMemory(id) {
// eslint-disable-next-line no-alert
if (!window.confirm(t('CAPTAIN.MEMORY.CONFIRM_DELETE'))) return;
await ContactMemoriesAPI.destroy(props.contactId, id);
await load();
}
async function forgetAll() {
// eslint-disable-next-line no-alert
if (!window.confirm(t('CAPTAIN.MEMORY.CONFIRM_FORGET_ALL'))) return;
await ContactMemoriesAPI.forgetAll(props.contactId);
await load();
}
onMounted(load);
</script>
<template>
<div class="contact-memories">
<div v-if="loading">{{ t('CAPTAIN.MEMORY.LOADING') }}</div>
<div v-else-if="error" class="error">{{ error }}</div>
<div v-else-if="!memories.length" class="empty">
{{ t('CAPTAIN.MEMORY.EMPTY') }}
</div>
<ul v-else>
<li v-for="m in memories" :key="m.id" class="memory-row">
<span class="type">{{ m.memory_type }}</span>
<span class="content">{{ m.content }}</span>
<span class="confidence">{{ Math.round(m.confidence * 100) }}%</span>
<button @click="deleteMemory(m.id)">
{{ t('CAPTAIN.MEMORY.FORGET') }}
</button>
</li>
</ul>
<button class="danger" @click="forgetAll">
{{ t('CAPTAIN.MEMORY.FORGET_ALL') }}
</button>
</div>
</template>