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:
parent
8444209952
commit
2f7d8edd92
34
app/javascript/dashboard/api/captain/contactMemories.js
Normal file
34
app/javascript/dashboard/api/captain/contactMemories.js
Normal 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();
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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>
|
||||
Loading…
Reference in New Issue
Block a user