feat: Adiciona internacionalização para a base de conhecimento Jasmine, ajusta regras de lint e atualiza dependências.
This commit is contained in:
parent
2672d21136
commit
c0cd8c24b0
1
Gemfile
1
Gemfile
@ -60,6 +60,7 @@ gem 'aws-actionmailbox-ses', '~> 0'
|
||||
|
||||
##-- gems for database --#
|
||||
gem 'groupdate'
|
||||
gem 'fiddle'
|
||||
gem 'pg'
|
||||
gem 'redis'
|
||||
gem 'redis-namespace'
|
||||
|
||||
@ -338,6 +338,7 @@ GEM
|
||||
ffi-compiler (1.0.1)
|
||||
ffi (>= 1.0.0)
|
||||
rake
|
||||
fiddle (1.1.8)
|
||||
flag_shih_tzu (0.3.23)
|
||||
foreman (0.87.2)
|
||||
fugit (1.11.1)
|
||||
@ -1071,6 +1072,7 @@ DEPENDENCIES
|
||||
faker
|
||||
faraday_middleware-aws-sigv4
|
||||
fcm
|
||||
fiddle
|
||||
flag_shih_tzu
|
||||
foreman
|
||||
geocoder
|
||||
@ -1180,7 +1182,7 @@ DEPENDENCIES
|
||||
working_hours
|
||||
|
||||
RUBY VERSION
|
||||
ruby 3.4.4p34
|
||||
ruby 3.4.4p34
|
||||
|
||||
BUNDLED WITH
|
||||
2.5.5
|
||||
2.5.5
|
||||
|
||||
@ -46,23 +46,32 @@ class JasmineAPI extends ApiClient {
|
||||
}
|
||||
|
||||
unlinkCollection(inboxId, collectionId) {
|
||||
return axios.delete(`${this.url}/${inboxId}/jasmine/collections/${collectionId}`);
|
||||
return axios.delete(
|
||||
`${this.url}/${inboxId}/jasmine/collections/${collectionId}`
|
||||
);
|
||||
}
|
||||
|
||||
// Documents
|
||||
getDocuments(collectionId) {
|
||||
return axios.get(`${this.jasmineUrl}/collections/${collectionId}/documents`);
|
||||
return axios.get(
|
||||
`${this.jasmineUrl}/collections/${collectionId}/documents`
|
||||
);
|
||||
}
|
||||
|
||||
uploadDocument(collectionId, content, title) {
|
||||
return axios.post(`${this.jasmineUrl}/collections/${collectionId}/documents`, {
|
||||
title,
|
||||
content,
|
||||
});
|
||||
return axios.post(
|
||||
`${this.jasmineUrl}/collections/${collectionId}/documents`,
|
||||
{
|
||||
title,
|
||||
content,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
deleteDocument(collectionId, documentId) {
|
||||
return axios.delete(`${this.jasmineUrl}/collections/${collectionId}/documents/${documentId}`);
|
||||
return axios.delete(
|
||||
`${this.jasmineUrl}/collections/${collectionId}/documents/${documentId}`
|
||||
);
|
||||
}
|
||||
|
||||
// Playground
|
||||
@ -85,4 +94,3 @@ class JasmineAPI extends ApiClient {
|
||||
}
|
||||
|
||||
export default new JasmineAPI();
|
||||
|
||||
|
||||
@ -109,7 +109,8 @@ export function usePolicy() {
|
||||
if (!flag) return false;
|
||||
|
||||
// Bypass paywall for Captain in development
|
||||
if (['captain_integration', 'captain_integration_v2'].includes(flag)) return false;
|
||||
if (['captain_integration', 'captain_integration_v2'].includes(flag))
|
||||
return false;
|
||||
|
||||
if (isACustomBrandedInstance.value) {
|
||||
// custom branded instances never show paywall
|
||||
|
||||
@ -40,6 +40,7 @@ import whatsappTemplates from './whatsappTemplates.json';
|
||||
import contentTemplates from './contentTemplates.json';
|
||||
import mfa from './mfa.json';
|
||||
import yearInReview from './yearInReview.json';
|
||||
import jasmine from './jasmine.json';
|
||||
|
||||
export default {
|
||||
...advancedFilters,
|
||||
@ -84,4 +85,5 @@ export default {
|
||||
...contentTemplates,
|
||||
...mfa,
|
||||
...yearInReview,
|
||||
...jasmine,
|
||||
};
|
||||
|
||||
72
app/javascript/dashboard/i18n/locale/en/jasmine.json
Normal file
72
app/javascript/dashboard/i18n/locale/en/jasmine.json
Normal file
@ -0,0 +1,72 @@
|
||||
{
|
||||
"JASMINE": {
|
||||
"HEADER": {
|
||||
"TITLE": "Jasmine AI Agents",
|
||||
"DESCRIPTION": "Manage your AI SDR agents. Select an inbox to configure your knowledge base.",
|
||||
"EMPTY": "No inboxes found"
|
||||
},
|
||||
"CONFIG": {
|
||||
"TITLE": "Jasmine AI Configuration",
|
||||
"DESCRIPTION": "Configure the AI agent for this inbox.",
|
||||
"ENABLE": "Enable Jasmine AI Agent",
|
||||
"SYSTEM_PROMPT": "System Prompt",
|
||||
"SYSTEM_PROMPT_HELP": "Define the persona and behavioral rules for the agent.",
|
||||
"UPDATE_BUTTON": "Update Configuration"
|
||||
},
|
||||
"KNOWLEDGE_BASE": {
|
||||
"TITLE": "Knowledge Base",
|
||||
"DESCRIPTION": "Manage knowledge collections for this inbox",
|
||||
"ADD_BUTTON": "+ New Collection",
|
||||
"DOCUMENTS": "Documents",
|
||||
"LOADING_DOCS": "Loading documents...",
|
||||
"UNTITLED_DOC": "Untitled Document",
|
||||
"NO_DOCS": "No documents yet. Add your first document below.",
|
||||
"ADD_DOC_HEADER": "Add New Document",
|
||||
"DOC_TITLE_PLACEHOLDER": "Document title (optional)",
|
||||
"DOC_CONTENT_PLACEHOLDER": "Paste or type your knowledge content here...",
|
||||
"ADD_DOC_BUTTON": "Add Document",
|
||||
"NO_COLLECTIONS": "No collections yet. Create one to get started.",
|
||||
"CREATE_MODAL": {
|
||||
"TITLE": "Create Collection",
|
||||
"NAME_PLACEHOLDER": "Collection name",
|
||||
"VISIBILITY_PRIVATE": "Private (This inbox only)",
|
||||
"VISIBILITY_SHARED": "Shared (All inboxes)",
|
||||
"CANCEL": "Cancel",
|
||||
"CREATE": "Create"
|
||||
},
|
||||
"DELETE_CONFIRM": "Are you sure you want to delete this document?",
|
||||
"DOCUMENT_DELETE_SUCCESS": "Document deleted successfully",
|
||||
"COLLECTION_DELETE_SUCCESS": "Collection deleted successfully",
|
||||
"SAVE_SUCCESS": "Changes saved successfully",
|
||||
"DOCUMENT_CREATE_SUCCESS": "Document created successfully",
|
||||
"COLLECTION_CREATE_SUCCESS": "Collection created successfully"
|
||||
},
|
||||
"PLAYGROUND": {
|
||||
"TITLE": "Jasmine AI Playground",
|
||||
"DESCRIPTION": "Test Jasmine responses in real-time before enabling for customers.",
|
||||
"SELECT_INBOX": "Select an Inbox to test",
|
||||
"CHOOSE_INBOX": "Choose an inbox...",
|
||||
"WARNING": "Make sure Jasmine is enabled and configured for this inbox",
|
||||
"EMPTY_STATE_TITLE": "Send a message to test Jasmine",
|
||||
"EMPTY_STATE_EXAMPLES": "Try: \"Hello\", \"How much does it cost?\", \"How does it work?\"",
|
||||
"LOADING": "Jasmine is thinking...",
|
||||
"INPUT_PLACEHOLDER": "Type a test message...",
|
||||
"CLEAR_TOOLTIP": "Clear conversation",
|
||||
"NO_INBOX_SELECTED": "Select an inbox above to start testing"
|
||||
},
|
||||
"INBOX_LIST": {
|
||||
"ACTIVE": "Active",
|
||||
"CONFIGURE": "Configure",
|
||||
"DESCRIPTION": "Channel {channel} configured for Jasmine AI"
|
||||
},
|
||||
"WUZAPI": {
|
||||
"STATUS": "Status: {status}",
|
||||
"ACCOUNT_ERROR": "Error: Account ID not loaded. Please refresh the page.",
|
||||
"CONNECT_FALLBACK": "Click to initiate connection",
|
||||
"CONNECT_BUTTON_FALLBACK": "Connect WhatsApp",
|
||||
"WEBHOOK_SECTION": "Webhook Configuration",
|
||||
"GET_WEBHOOK_INFO": "Get Webhook Info",
|
||||
"UPDATE_WEBHOOK": "Update Webhook Connection"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -44,4 +44,3 @@ const routes = [
|
||||
];
|
||||
|
||||
export default routes;
|
||||
|
||||
|
||||
@ -45,8 +45,8 @@ const getChannelName = channelType => {
|
||||
>
|
||||
<template #header>
|
||||
<BaseSettingsHeader
|
||||
title="Agentes Jasmine AI"
|
||||
description="Gerencie seus agentes de IA SDR. Selecione uma caixa de entrada para configurar sua base de conhecimento."
|
||||
:title="$t('JASMINE.HEADER.TITLE')"
|
||||
:description="$t('JASMINE.HEADER.DESCRIPTION')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@ -64,14 +64,12 @@ const getChannelName = channelType => {
|
||||
class="flex items-center justify-center size-12 rounded-lg bg-n-blue-2"
|
||||
>
|
||||
<span
|
||||
:class="[
|
||||
getChannelIcon(inbox.channel_type),
|
||||
'size-6 text-n-blue-text',
|
||||
]"
|
||||
class="size-6 text-n-blue-text"
|
||||
:class="[getChannelIcon(inbox.channel_type)]"
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
v-tooltip="'Ativo'"
|
||||
v-tooltip="$t('JASMINE.INBOX_LIST.ACTIVE')"
|
||||
class="text-white p-0.5 rounded-full size-5 flex items-center justify-center bg-n-teal-9"
|
||||
>
|
||||
<i class="i-ph-check-bold text-sm" />
|
||||
@ -83,13 +81,20 @@ const getChannelName = channelType => {
|
||||
<span class="text-base font-semibold text-n-slate-12">{{
|
||||
inbox.name
|
||||
}}</span>
|
||||
<Button label="Configurar" link @click.stop="openInbox(inbox.id)" />
|
||||
<Button
|
||||
:label="$t('JASMINE.INBOX_LIST.CONFIGURE')"
|
||||
link
|
||||
@click.stop="openInbox(inbox.id)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<p class="text-sm text-n-slate-11">
|
||||
Canal {{ getChannelName(inbox.channel_type) }} configurado para
|
||||
Jasmine AI
|
||||
{{
|
||||
$t('JASMINE.INBOX_LIST.DESCRIPTION', {
|
||||
channel: getChannelName(inbox.channel_type),
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -63,8 +63,8 @@ const clearChat = () => {
|
||||
<SettingsLayout :is-loading="false">
|
||||
<template #header>
|
||||
<BaseSettingsHeader
|
||||
title="Playground Jasmine AI"
|
||||
description="Teste as respostas da Jasmine em tempo real antes de ativar para os clientes."
|
||||
:title="$t('JASMINE.PLAYGROUND.TITLE')"
|
||||
:description="$t('JASMINE.PLAYGROUND.DESCRIPTION')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@ -73,20 +73,25 @@ const clearChat = () => {
|
||||
<!-- Inbox Selector -->
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-n-slate-12 mb-2">
|
||||
Selecione uma Inbox para testar
|
||||
{{ $t('JASMINE.PLAYGROUND.SELECT_INBOX') }}
|
||||
</label>
|
||||
<select
|
||||
v-model="selectedInboxId"
|
||||
class="w-full max-w-md px-3 py-2 text-sm rounded-lg border border-n-weak bg-n-solid-1 text-n-slate-12"
|
||||
>
|
||||
<option :value="null">Escolha uma inbox...</option>
|
||||
<option :value="null">
|
||||
{{ $t('JASMINE.PLAYGROUND.CHOOSE_INBOX') }}
|
||||
</option>
|
||||
<option v-for="inbox in inboxes" :key="inbox.id" :value="inbox.id">
|
||||
{{ inbox.name }}
|
||||
</option>
|
||||
</select>
|
||||
<p v-if="selectedInbox" class="text-xs text-n-slate-11 mt-1">
|
||||
⚠️ Certifique-se de que a Jasmine está ativada e configurada para
|
||||
esta inbox
|
||||
{{
|
||||
$t('JASMINE.PLAYGROUND.FETCH_ERROR', {
|
||||
error: $t('JASMINE.PLAYGROUND.WARNING'),
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -102,17 +107,17 @@ const clearChat = () => {
|
||||
class="text-center text-n-slate-11 py-12"
|
||||
>
|
||||
<span class="i-lucide-message-square size-12 mb-4 opacity-50" />
|
||||
<p>Envie uma mensagem para testar a Jasmine</p>
|
||||
<p>{{ $t('JASMINE.PLAYGROUND.EMPTY_STATE_TITLE') }}</p>
|
||||
<p class="text-xs mt-2">
|
||||
Experimente: "Olá", "Quanto custa?", "Como funciona?"
|
||||
{{ $t('JASMINE.PLAYGROUND.EMPTY_STATE_EXAMPLES') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="(msg, index) in messages"
|
||||
:key="index"
|
||||
class="max-w-[80%] rounded-lg p-3"
|
||||
:class="[
|
||||
'max-w-[80%] rounded-lg p-3',
|
||||
msg.role === 'user'
|
||||
? 'ml-auto bg-n-blue-9 text-white'
|
||||
: msg.role === 'error'
|
||||
@ -125,10 +130,16 @@ const clearChat = () => {
|
||||
v-if="msg.debug"
|
||||
class="mt-2 pt-2 border-t border-n-weak text-xs text-n-slate-11"
|
||||
>
|
||||
<span class="font-mono"
|
||||
>{{ msg.debug.model }} | temp:
|
||||
{{ msg.debug.temperature }}</span
|
||||
>
|
||||
<span class="font-mono">
|
||||
{{
|
||||
$t('JASMINE.PLAYGROUND.MODEL', { model: msg.debug.model })
|
||||
}}
|
||||
{{
|
||||
$t('JASMINE.PLAYGROUND.TEMPERATURE', {
|
||||
temp: msg.debug.temperature,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -137,7 +148,9 @@ const clearChat = () => {
|
||||
class="flex items-center gap-2 text-n-slate-11"
|
||||
>
|
||||
<span class="i-lucide-loader-2 size-4 animate-spin" />
|
||||
<span class="text-sm">Jasmine está pensando...</span>
|
||||
<span class="text-sm">{{
|
||||
$t('JASMINE.PLAYGROUND.LOADING')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -148,7 +161,7 @@ const clearChat = () => {
|
||||
v-model="inputMessage"
|
||||
type="text"
|
||||
class="flex-1 px-3 py-2 text-sm rounded-lg border border-n-weak bg-n-solid-1 text-n-slate-12"
|
||||
placeholder="Digite uma mensagem de teste..."
|
||||
:placeholder="$t('JASMINE.PLAYGROUND.INPUT_PLACEHOLDER')"
|
||||
:disabled="isLoading"
|
||||
@keyup.enter="sendMessage"
|
||||
/>
|
||||
@ -158,7 +171,7 @@ const clearChat = () => {
|
||||
@click="sendMessage"
|
||||
/>
|
||||
<Button
|
||||
v-tooltip="'Limpar conversa'"
|
||||
v-tooltip="$t('JASMINE.PLAYGROUND.CLEAR_TOOLTIP')"
|
||||
icon="i-lucide-trash-2"
|
||||
faded
|
||||
slate
|
||||
@ -176,7 +189,7 @@ const clearChat = () => {
|
||||
>
|
||||
<div class="text-center">
|
||||
<span class="i-lucide-inbox size-16 mb-4 opacity-30" />
|
||||
<p>Selecione uma inbox acima para começar a testar</p>
|
||||
<p>{{ $t('JASMINE.PLAYGROUND.NO_INBOX_SELECTED') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'JasmineWrapper',
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
@ -70,10 +70,10 @@ export default {
|
||||
<div class="settings-section">
|
||||
<div class="flex flex-col gap-1 items-start mb-4">
|
||||
<h2 class="text-xl font-medium text-slate-900 dark:text-slate-100">
|
||||
Jasmine AI Configuration
|
||||
{{ $t('JASMINE.CONFIG.TITLE') }}
|
||||
</h2>
|
||||
<p class="text-sm text-slate-600 dark:text-slate-400">
|
||||
Configure the AI agent for this inbox.
|
||||
{{ $t('JASMINE.CONFIG.DESCRIPTION') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -85,7 +85,7 @@ export default {
|
||||
class="form-checkbox h-5 w-5 text-woot-500 rounded border-gray-300 focus:ring-woot-500"
|
||||
/>
|
||||
<span class="text-sm font-medium text-slate-700 dark:text-slate-200">
|
||||
Enable Jasmine AI Agent
|
||||
{{ $t('JASMINE.CONFIG.ENABLE') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
@ -94,21 +94,21 @@ export default {
|
||||
<label
|
||||
class="block text-sm font-medium text-slate-700 dark:text-slate-200 mb-2"
|
||||
>
|
||||
System Prompt
|
||||
{{ $t('JASMINE.CONFIG.SYSTEM_PROMPT') }}
|
||||
</label>
|
||||
<textarea
|
||||
v-model="systemPrompt"
|
||||
rows="6"
|
||||
class="w-full text-sm rounded-md border-gray-300 dark:border-slate-700 dark:bg-slate-900 focus:border-woot-500 focus:ring-woot-500"
|
||||
placeholder="You are a helpful SDR agent..."
|
||||
></textarea>
|
||||
:placeholder="$t('JASMINE.CONFIG.SYSTEM_PROMPT_HELP')"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-slate-500">
|
||||
Define the persona and behavioral rules for the agent.
|
||||
{{ $t('JASMINE.CONFIG.SYSTEM_PROMPT_HELP') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<woot-button :is-loading="isUpdating" @click="updateSettings">
|
||||
Update Configuration
|
||||
{{ $t('JASMINE.CONFIG.UPDATE_BUTTON') }}
|
||||
</woot-button>
|
||||
|
||||
<JasmineKnowledgeBase v-if="showKnowledgeBase" :inbox-id="inbox.id" />
|
||||
|
||||
@ -34,20 +34,25 @@ export default defineComponent({
|
||||
return `/api/v1/accounts/${accountId.value}/inboxes/${props.inbox.id}/wuzapi${endpoint}`;
|
||||
};
|
||||
|
||||
const fetchStatus = async () => {
|
||||
function stopPolling() {
|
||||
if (pollInterval) {
|
||||
clearInterval(pollInterval);
|
||||
pollInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchStatus() {
|
||||
if (!accountId.value) return;
|
||||
|
||||
try {
|
||||
const response = await window.axios.get(getApiUrl(''));
|
||||
|
||||
const data = response.data;
|
||||
// Wuzapi format: { data: { connected: true, jid: "...", details: "..." } }
|
||||
const wuzapiData = data.data || {};
|
||||
|
||||
const isWuzapiConnected =
|
||||
wuzapiData.connected === true && !!wuzapiData.jid;
|
||||
|
||||
// Also keep legacy check just in case payload differs
|
||||
const legacyStatus = data.status || data.state;
|
||||
const isLegacyConnected = ['CONNECTED', 'inChat', 'success'].includes(
|
||||
legacyStatus
|
||||
@ -64,13 +69,22 @@ export default defineComponent({
|
||||
statusMessage.value =
|
||||
error.response?.data?.error || error.message || 'Check failed';
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const fetchQrCode = async () => {
|
||||
/* eslint-disable no-use-before-define */
|
||||
function startPolling() {
|
||||
if (pollInterval) return;
|
||||
pollInterval = setInterval(async () => {
|
||||
await fetchStatus();
|
||||
if (pollInterval && !isConnected.value) {
|
||||
await fetchQrCode();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
async function fetchQrCode() {
|
||||
try {
|
||||
const response = await window.axios.get(getApiUrl('/qr'));
|
||||
|
||||
// Backend now normalizes to 'qrcode' in most cases, but we keep robust checks
|
||||
const d = response.data;
|
||||
const qrcodeData =
|
||||
d.qrcode ||
|
||||
@ -84,7 +98,6 @@ export default defineComponent({
|
||||
qrCode.value = qrcodeData;
|
||||
startPolling();
|
||||
} else {
|
||||
// Fallback: maybe we are already connected?
|
||||
await fetchStatus();
|
||||
if (!isConnected.value) {
|
||||
statusMessage.value = 'QR Code not received and not connected.';
|
||||
@ -94,7 +107,7 @@ export default defineComponent({
|
||||
statusMessage.value =
|
||||
error.response?.data?.error || 'Failed to load QR';
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleConnect = async () => {
|
||||
if (!accountId.value) {
|
||||
@ -131,26 +144,6 @@ export default defineComponent({
|
||||
}
|
||||
};
|
||||
|
||||
// Function hoisting allows use before definition
|
||||
function stopPolling() {
|
||||
if (pollInterval) {
|
||||
clearInterval(pollInterval);
|
||||
pollInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
function startPolling() {
|
||||
if (pollInterval) return;
|
||||
// Poll every 5 seconds to check status AND refresh QR code
|
||||
pollInterval = setInterval(async () => {
|
||||
await fetchStatus();
|
||||
// If still not connected (and polling hasn't been stopped by fetchStatus), refresh QR
|
||||
if (pollInterval && !isConnected.value) {
|
||||
await fetchQrCode();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
const isLoadingWebhook = ref(false);
|
||||
const webhookInfo = ref(null);
|
||||
|
||||
@ -173,7 +166,7 @@ export default defineComponent({
|
||||
const response = await window.axios.put(getApiUrl('/update_webhook'));
|
||||
webhookInfo.value = {
|
||||
message: response.data.message,
|
||||
url: response.data.webhook_url
|
||||
url: response.data.webhook_url,
|
||||
};
|
||||
useAlert('Webhook updated successfully');
|
||||
} catch (error) {
|
||||
@ -213,8 +206,8 @@ export default defineComponent({
|
||||
<div class="mx-8 mt-6">
|
||||
<div class="bg-white p-6 rounded-lg border border-n-weak">
|
||||
<h3 class="text-lg font-medium text-n-slate-12 mb-4">
|
||||
{{ $t('INBOX_MGMT.ADD.WHATSAPP.PROVIDERS.WUZAPI') }} -
|
||||
{{ $t('INBOX_MGMT.SETTINGS_POPUP.MESSENGER_CONFIG') }}
|
||||
{{ $t('INBOX_MGMT.ADD.WHATSAPP.PROVIDERS.WUZAPI') }}
|
||||
{{ `- ${$t('INBOX_MGMT.SETTINGS_POPUP.MESSENGER_CONFIG')}` }}
|
||||
</h3>
|
||||
|
||||
<div v-if="accountId" class="flex flex-col items-center">
|
||||
@ -264,29 +257,29 @@ export default defineComponent({
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-xs text-n-slate-10">
|
||||
Status: {{ statusMessage }}
|
||||
{{ $t('JASMINE.WUZAPI.STATUS', { status: statusMessage }) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="text-red-600 p-4">
|
||||
Error: Account ID not loaded. Please refresh the page.
|
||||
{{ $t('JASMINE.WUZAPI.ACCOUNT_ERROR') }}
|
||||
</div>
|
||||
<div class="mt-8 pt-6 border-t border-n-weak w-full">
|
||||
<h4 class="text-md font-medium text-n-slate-12 mb-4">
|
||||
Webhook Configuration
|
||||
{{ $t('JASMINE.WUZAPI.WEBHOOK_SECTION') }}
|
||||
</h4>
|
||||
<div class="flex gap-4 mb-4">
|
||||
<NextButton
|
||||
icon="i-woot-refresh"
|
||||
:is-loading="isLoadingWebhook"
|
||||
label="Get Webhook Info"
|
||||
:label="$t('JASMINE.WUZAPI.GET_WEBHOOK_INFO')"
|
||||
@click="fetchWebhookInfo"
|
||||
/>
|
||||
<NextButton
|
||||
icon="i-woot-upload"
|
||||
:is-loading="isLoadingWebhook"
|
||||
label="Update Webhook Connection"
|
||||
:label="$t('JASMINE.WUZAPI.UPDATE_WEBHOOK')"
|
||||
@click="updateWebhook"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -101,11 +101,13 @@ export default {
|
||||
}
|
||||
},
|
||||
async deleteDocument(collectionId, documentId) {
|
||||
if (!confirm('Are you sure you want to delete this document?')) return;
|
||||
// eslint-disable-next-line no-alert
|
||||
if (!window.confirm(this.$t('JASMINE.KNOWLEDGE_BASE.DELETE_CONFIRM')))
|
||||
return;
|
||||
this.isDeletingDocument = documentId;
|
||||
try {
|
||||
await JasmineAPI.deleteDocument(collectionId, documentId);
|
||||
useAlert('Document deleted successfully');
|
||||
useAlert(this.$t('JASMINE.KNOWLEDGE_BASE.DOCUMENT_DELETE_SUCCESS'));
|
||||
this.fetchDocuments(collectionId);
|
||||
} catch (error) {
|
||||
useAlert('Failed to delete document');
|
||||
@ -143,14 +145,14 @@ export default {
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-slate-900 dark:text-slate-100">
|
||||
Knowledge Base
|
||||
{{ $t('JASMINE.KNOWLEDGE_BASE.TITLE') }}
|
||||
</h3>
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400">
|
||||
Manage knowledge collections for this inbox
|
||||
{{ $t('JASMINE.KNOWLEDGE_BASE.DESCRIPTION') }}
|
||||
</p>
|
||||
</div>
|
||||
<woot-button size="small" @click="showCreateCollectionModal = true">
|
||||
+ New Collection
|
||||
{{ $t('JASMINE.KNOWLEDGE_BASE.ADD_BUTTON') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
|
||||
@ -173,8 +175,8 @@ export default {
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<span
|
||||
class="i-lucide-chevron-right size-4 transition-transform text-slate-400"
|
||||
:class="[
|
||||
'i-lucide-chevron-right size-4 transition-transform text-slate-400',
|
||||
expandedCollectionId === collection.id ? 'rotate-90' : '',
|
||||
]"
|
||||
/>
|
||||
@ -197,7 +199,7 @@ export default {
|
||||
class="border-t border-slate-100 dark:border-slate-700 bg-slate-50 dark:bg-slate-800/30 p-4"
|
||||
>
|
||||
<h5 class="text-xs font-semibold uppercase text-slate-500 mb-3">
|
||||
Documents
|
||||
{{ $t('JASMINE.KNOWLEDGE_BASE.DOCUMENTS') }}
|
||||
</h5>
|
||||
|
||||
<!-- Loading Documents -->
|
||||
@ -206,7 +208,7 @@ export default {
|
||||
class="flex items-center gap-2 text-sm text-slate-400 py-2"
|
||||
>
|
||||
<span class="i-lucide-loader-2 size-4 animate-spin" />
|
||||
Loading documents...
|
||||
{{ $t('JASMINE.KNOWLEDGE_BASE.LOADING_DOCS') }}
|
||||
</div>
|
||||
|
||||
<!-- Documents List -->
|
||||
@ -224,7 +226,7 @@ export default {
|
||||
<p
|
||||
class="font-medium text-sm text-slate-800 dark:text-slate-200 truncate"
|
||||
>
|
||||
{{ doc.title || 'Untitled Document' }}
|
||||
{{ doc.title || $t('JASMINE.KNOWLEDGE_BASE.UNTITLED_DOC') }}
|
||||
</p>
|
||||
<p class="text-xs text-slate-400 truncate">
|
||||
{{ new Date(doc.created_at).toLocaleDateString() }}
|
||||
@ -234,10 +236,8 @@ export default {
|
||||
<div class="flex items-center gap-3 shrink-0">
|
||||
<!-- Status Badge -->
|
||||
<span
|
||||
:class="[
|
||||
'inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium rounded-full',
|
||||
getStatusClass(doc.status),
|
||||
]"
|
||||
class="inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium rounded-full"
|
||||
:class="[getStatusClass(doc.status)]"
|
||||
>
|
||||
<span
|
||||
v-if="isProcessing(doc.status)"
|
||||
@ -264,7 +264,7 @@ export default {
|
||||
v-if="documents.length === 0"
|
||||
class="text-center py-6 text-sm text-slate-400"
|
||||
>
|
||||
No documents yet. Add your first document below.
|
||||
{{ $t('JASMINE.KNOWLEDGE_BASE.NO_DOCS') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -273,19 +273,21 @@ export default {
|
||||
class="border-t border-slate-200 dark:border-slate-700 pt-4 mt-4"
|
||||
>
|
||||
<h6 class="text-xs font-semibold uppercase text-slate-500 mb-3">
|
||||
Add New Document
|
||||
{{ $t('JASMINE.KNOWLEDGE_BASE.ADD_DOC_HEADER') }}
|
||||
</h6>
|
||||
<input
|
||||
v-model="newDocTitle"
|
||||
type="text"
|
||||
class="w-full mb-2 px-3 py-2 text-sm rounded-lg border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900"
|
||||
placeholder="Document title (optional)"
|
||||
:placeholder="$t('JASMINE.KNOWLEDGE_BASE.DOC_TITLE_PLACEHOLDER')"
|
||||
/>
|
||||
<textarea
|
||||
v-model="newDocContent"
|
||||
rows="4"
|
||||
class="w-full mb-3 px-3 py-2 text-sm rounded-lg border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 resize-none"
|
||||
placeholder="Paste or type your knowledge content here..."
|
||||
:placeholder="
|
||||
$t('JASMINE.KNOWLEDGE_BASE.DOC_CONTENT_PLACEHOLDER')
|
||||
"
|
||||
/>
|
||||
<div class="flex justify-end">
|
||||
<woot-button
|
||||
@ -294,7 +296,7 @@ export default {
|
||||
:disabled="!newDocContent.trim()"
|
||||
@click="addDocument(collection.id)"
|
||||
>
|
||||
Add Document
|
||||
{{ $t('JASMINE.KNOWLEDGE_BASE.ADD_DOC_BUTTON') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
@ -307,7 +309,7 @@ export default {
|
||||
class="text-center py-12 text-slate-400"
|
||||
>
|
||||
<span class="i-lucide-folder-open size-12 mx-auto mb-3 opacity-50" />
|
||||
<p class="text-sm">No collections yet. Create one to get started.</p>
|
||||
<p class="text-sm">{{ $t('JASMINE.KNOWLEDGE_BASE.NO_COLLECTIONS') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -319,34 +321,40 @@ export default {
|
||||
>
|
||||
<div class="bg-white dark:bg-slate-900 p-6 rounded-xl w-96 shadow-2xl">
|
||||
<h3 class="text-lg font-semibold mb-4 text-slate-900 dark:text-white">
|
||||
Create Collection
|
||||
{{ $t('JASMINE.KNOWLEDGE_BASE.CREATE_MODAL.TITLE') }}
|
||||
</h3>
|
||||
<input
|
||||
v-model="newCollectionName"
|
||||
type="text"
|
||||
class="w-full mb-4 px-3 py-2 rounded-lg border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800"
|
||||
placeholder="Collection name"
|
||||
:placeholder="
|
||||
$t('JASMINE.KNOWLEDGE_BASE.CREATE_MODAL.NAME_PLACEHOLDER')
|
||||
"
|
||||
@keyup.enter="createCollection"
|
||||
/>
|
||||
<select
|
||||
v-model="newCollectionVisibility"
|
||||
class="w-full mb-4 px-3 py-2 rounded-lg border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800"
|
||||
>
|
||||
<option value="private">Private (This inbox only)</option>
|
||||
<option value="shared">Shared (All inboxes)</option>
|
||||
<option value="private">
|
||||
{{ $t('JASMINE.KNOWLEDGE_BASE.CREATE_MODAL.VISIBILITY_PRIVATE') }}
|
||||
</option>
|
||||
<option value="shared">
|
||||
{{ $t('JASMINE.KNOWLEDGE_BASE.CREATE_MODAL.VISIBILITY_SHARED') }}
|
||||
</option>
|
||||
</select>
|
||||
<div class="flex justify-end gap-2">
|
||||
<woot-button
|
||||
variant="clear"
|
||||
@click="showCreateCollectionModal = false"
|
||||
>
|
||||
Cancel
|
||||
{{ $t('JASMINE.KNOWLEDGE_BASE.CREATE_MODAL.CANCEL') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
:disabled="!newCollectionName.trim()"
|
||||
@click="createCollection"
|
||||
>
|
||||
Create
|
||||
{{ $t('JASMINE.KNOWLEDGE_BASE.CREATE_MODAL.CREATE') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -77,7 +77,7 @@ class Inbox < ApplicationRecord
|
||||
has_one :agent_bot, through: :agent_bot_inbox
|
||||
has_many :webhooks, dependent: :destroy_async
|
||||
has_many :hooks, dependent: :destroy_async, class_name: 'Integrations::Hook'
|
||||
has_many :inbox_automations, dependent: :destroy_async, class_name: 'Captain::InboxAutomation'
|
||||
has_many :inbox_automations, dependent: :destroy_async, class_name: '::Captain::InboxAutomation'
|
||||
|
||||
# Jasmine
|
||||
has_one :jasmine_inbox_config, class_name: 'Jasmine::InboxConfig', dependent: :destroy
|
||||
|
||||
@ -12,7 +12,8 @@ Rails.application.config.to_prepare do
|
||||
|
||||
def ensure_content_presence_defensive
|
||||
# If content is present, or we have attachments, we are good.
|
||||
return if content.present? || attachments.any?
|
||||
# We check .any? and .size to be robust against unsaved attachments in some contexts.
|
||||
return if content.present? || attachments.any? || attachments.size > 0 || attachments.to_a.any?
|
||||
|
||||
# Identifica a origem para um fallback mais inteligente
|
||||
if incoming?
|
||||
|
||||
@ -5,9 +5,9 @@ module Enterprise::Concerns::Inbox
|
||||
has_one :captain_inbox, dependent: :destroy, class_name: 'CaptainInbox'
|
||||
has_one :captain_assistant,
|
||||
through: :captain_inbox,
|
||||
class_name: 'Captain::Assistant'
|
||||
has_one :captain_inbox_reminder_setting, dependent: :destroy, class_name: 'Captain::InboxReminderSetting'
|
||||
has_many :captain_inbox_automations, dependent: :destroy, class_name: 'Captain::InboxAutomation'
|
||||
class_name: '::Captain::Assistant'
|
||||
has_one :captain_inbox_reminder_setting, dependent: :destroy, class_name: '::Captain::InboxReminderSetting'
|
||||
has_many :captain_inbox_automations, dependent: :destroy, class_name: '::Captain::InboxAutomation'
|
||||
has_many :inbox_capacity_limits, dependent: :destroy
|
||||
end
|
||||
end
|
||||
|
||||
1
eslint_report.json
Normal file
1
eslint_report.json
Normal file
File diff suppressed because one or more lines are too long
1
eslint_report_v2.json
Normal file
1
eslint_report_v2.json
Normal file
File diff suppressed because one or more lines are too long
38
progresso/2025-05-lint-global-fix.md
Normal file
38
progresso/2025-05-lint-global-fix.md
Normal file
@ -0,0 +1,38 @@
|
||||
# Nota de Resolução: Correção Global de Lint e Qualidade (Maio 2025)
|
||||
|
||||
## Objetivo
|
||||
|
||||
Resolver mais de 100 erros de linting (Frontend e Backend) e estabelecer padrões de qualidade para os módulos Jasmine e Wuzapi.
|
||||
|
||||
## Contexto
|
||||
|
||||
O projeto apresentava débitos técnicos acumulados nas novas rotas de IA (Jasmine), incluindo falta de internacionalização, erros de sintaxe no Vue (hoisting/circular dependencies) e riscos de segurança no backend Ruby.
|
||||
|
||||
## Passos Realizados
|
||||
|
||||
1. **Frontend (Vue/ESLint)**:
|
||||
- Criação do sistema de i18n para Jasmine em `en/jasmine.json`.
|
||||
- Refatoração completa de 5 componentes (`JasmineInboxes`, `JasminePlayground`, `JasmineConfiguration`, `JasmineKnowledgeBase`, `WuzapiConfiguration`).
|
||||
- Resolução de erros de Prettier e sintaxe.
|
||||
2. **Backend (Ruby/RuboCop)**:
|
||||
- Criação de `.rubocop_todo.yml` para congelar débitos antigos.
|
||||
- Refatoração de controllers e remoção de credenciais hardcoded.
|
||||
- Identificação de vulnerabilidade crítica de SSL em `lib/wuzapi/client.rb`.
|
||||
|
||||
## Arquivos Principais Alterados
|
||||
|
||||
- `app/javascript/dashboard/i18n/locale/en/jasmine.json` (Novo sistema i18n)
|
||||
- `app/javascript/dashboard/routes/dashboard/settings/inbox/channels/wuzapi/WuzapiConfiguration.vue` (Fix Hosting)
|
||||
- `lib/wuzapi/client.rb` (Identificado risco SSL)
|
||||
- `.rubocop_todo.yml` (Gestão de débito técnico)
|
||||
|
||||
## Variáveis de Ambiente
|
||||
|
||||
- `DEFAULT_JASMINE_DISTANCE_THRESHOLD`: Configura limite de busca RAG (Padrão: 0.35).
|
||||
- `JASMINE_LLM_MODEL`: Define modelo de IA (Padrão: gpt-4o-mini).
|
||||
|
||||
## Como Validar ou Reverter
|
||||
|
||||
- **Validar Frontend**: Executar `npx eslint --ext .js,.vue [arquivos]`. Resultado esperado: 0 erros.
|
||||
- **Validar Backend**: Executar `bundle exec rubocop`. Resultado esperado: Sucesso (via todo).
|
||||
- **Reverter**: `git checkout` nos arquivos mencionados.
|
||||
@ -129,7 +129,7 @@ has been assigned to you"
|
||||
|
||||
it 'returns appropriate body suited for the notification type assigned_conversation_new_message when attachment message' do
|
||||
conversation = create(:conversation)
|
||||
message = create(:message, sender: create(:user), content: nil, conversation: conversation)
|
||||
message = build(:message, sender: create(:user), content: nil, conversation: conversation)
|
||||
attachment = message.attachments.new(file_type: :image, account_id: message.account_id)
|
||||
attachment.file.attach(io: Rails.root.join('spec/assets/avatar.png').open, filename: 'avatar.png', content_type: 'image/png')
|
||||
message.save!
|
||||
|
||||
@ -41,7 +41,7 @@ end
|
||||
RSpec.configure do |config|
|
||||
config.include FactoryBot::Syntax::Methods
|
||||
# Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
|
||||
config.fixture_path = Rails.root.join('spec/fixtures')
|
||||
config.fixture_paths = [Rails.root.join('spec/fixtures')]
|
||||
|
||||
# If you're not using ActiveRecord, or you'd prefer not to run each of your
|
||||
# examples within a transaction, remove the following line or assign false
|
||||
|
||||
Loading…
Reference in New Issue
Block a user