From 18a4bebca16da63b67facdf9071f369bd58c7fc5 Mon Sep 17 00:00:00 2001 From: Rodrigo Borba Date: Wed, 21 Jan 2026 11:14:47 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20Implementa=20e=20aprimora=20funcionalid?= =?UTF-8?q?ades=20do=20Captain=20para=20assistentes,=20cen=C3=A1rios,=20fe?= =?UTF-8?q?rramentas=20e=20reservas,=20al=C3=A9m=20de=20introduzir=20o=20d?= =?UTF-8?q?ashboard=20Jasmine=20com=20modelos.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .agent/workflows/formato.md | 7 + .../accounts/captain/assistants_controller.rb | 72 ++ .../accounts/captain/scenarios_controller.rb | 71 ++ .../v1/accounts/captain/tools_controller.rb | 73 ++ .../components-next/captain/PageLayout.vue | 14 +- .../assistant/AddNewScenariosDialog.vue | 86 +- .../reservations/CreateReservationModal.vue | 227 ++++ .../reservations/ReservationBoardColumn.vue | 81 ++ .../captain/reservations/ReservationCard.vue | 346 ++++++ .../components-next/textarea/TextArea.vue | 4 +- .../conversation/ConversationHeader.vue | 73 ++ .../dashboard/i18n/locale/en/captain.json | 48 + .../dashboard/i18n/locale/en/index.js | 2 + .../i18n/locale/en/integrations.json | 10 + .../dashboard/i18n/locale/pt_BR/captain.json | 48 + .../dashboard/i18n/locale/pt_BR/index.js | 4 + .../i18n/locale/pt_BR/integrations.json | 70 +- .../captain/assistants/scenarios/Index.vue | 7 + .../dashboard/captain/brands/BrandModal.vue | 393 +++--- .../routes/dashboard/captain/brands/Index.vue | 172 +-- .../captain/configurations/Index.vue | 29 +- .../dashboard/captain/extras/ExtraModal.vue | 40 +- .../routes/dashboard/captain/extras/Index.vue | 37 +- .../dashboard/captain/reservations/Index.vue | 1107 +++++++++++------ .../dashboard/captain/units/UnitModal.vue | 13 +- .../dashboard/jasmine/data/templates.js | 164 +++ .../jasmine/pages/JasmineInboxDashboard.vue | 81 +- app/models/account.rb | 1 + app/models/captain_assistant.rb | 6 + app/models/captain_document.rb | 10 + app/models/captain_scenario.rb | 13 + app/models/captain_tool_config.rb | 3 +- app/models/inbox.rb | 8 + .../captain/assistant/agent_runner_service.rb | 25 +- .../app/services/captain/llm/jasmine_brain.rb | 5 +- .../lib/captain/prompts/assistant.liquid | 6 + progresso/ajuste_erro_inboxes_vazias.md | 52 + progresso/correcao_ui_traducoes_brands.md | 57 + 38 files changed, 2678 insertions(+), 787 deletions(-) create mode 100644 .agent/workflows/formato.md create mode 100644 app/controllers/api/v1/accounts/captain/assistants_controller.rb create mode 100644 app/controllers/api/v1/accounts/captain/scenarios_controller.rb create mode 100644 app/controllers/api/v1/accounts/captain/tools_controller.rb create mode 100644 app/javascript/dashboard/components-next/captain/reservations/CreateReservationModal.vue create mode 100644 app/javascript/dashboard/components-next/captain/reservations/ReservationBoardColumn.vue create mode 100644 app/javascript/dashboard/components-next/captain/reservations/ReservationCard.vue create mode 100644 app/javascript/dashboard/i18n/locale/en/captain.json create mode 100644 app/javascript/dashboard/i18n/locale/pt_BR/captain.json create mode 100644 app/javascript/dashboard/routes/dashboard/jasmine/data/templates.js create mode 100644 app/models/captain_assistant.rb create mode 100644 app/models/captain_document.rb create mode 100644 app/models/captain_scenario.rb create mode 100644 progresso/ajuste_erro_inboxes_vazias.md create mode 100644 progresso/correcao_ui_traducoes_brands.md diff --git a/.agent/workflows/formato.md b/.agent/workflows/formato.md new file mode 100644 index 0000000..78bc055 --- /dev/null +++ b/.agent/workflows/formato.md @@ -0,0 +1,7 @@ +--- +description: interface_frontend +--- + +## Regra de Ouro de UI e i18n + +Nunca entregue ou sugira código de interface (Frontend) sem garantir que TODAS as strings visíveis tenham suas chaves de tradução devidamente criadas nos arquivos de locale (pt_BR e en). É proibido deixar chaves cruas (ex: `CAPTAIN.BRANDS...`) ou textos hardcoded na UI. Se criar uma nova feature, crie o arquivo JSON de tradução correspondente imediatamente. diff --git a/app/controllers/api/v1/accounts/captain/assistants_controller.rb b/app/controllers/api/v1/accounts/captain/assistants_controller.rb new file mode 100644 index 0000000..8237a44 --- /dev/null +++ b/app/controllers/api/v1/accounts/captain/assistants_controller.rb @@ -0,0 +1,72 @@ +module Api + module V1 + module Accounts + module Captain + class AssistantsController < Api::V1::Accounts::BaseController + before_action :fetch_assistant, only: [:show, :update, :destroy, :playground, :test_webhook] + + def index + @assistants = current_account.captain_assistants.order(created_at: :desc) + render json: @assistants + end + + def show + render json: @assistant + end + + def create + @assistant = current_account.captain_assistants.new(assistant_params) + if @assistant.save + render json: @assistant + else + render_error_response(@assistant) + end + end + + def update + if @assistant.update(assistant_params) + render json: @assistant + else + render_error_response(@assistant) + end + end + + def destroy + @assistant.destroy + head :ok + end + + def playground + # TODO: Implement playground logic + render json: { message: 'Playground not implemented yet' }, status: :ok + end + + def test_webhook + # TODO: Implement webhook test logic + render json: { message: 'Webhook test not implemented yet' }, status: :ok + end + + private + + def fetch_assistant + @assistant = current_account.captain_assistants.find(params[:id]) + end + + def assistant_params + params.require(:assistant).permit( + :name, + :description, + :llm_provider, + :llm_model, + :api_key, + config: {}, + response_guidelines: [], + guardrails: [], + handoff_webhook_config: {} + ) + end + end + end + end + end +end diff --git a/app/controllers/api/v1/accounts/captain/scenarios_controller.rb b/app/controllers/api/v1/accounts/captain/scenarios_controller.rb new file mode 100644 index 0000000..9b9137c --- /dev/null +++ b/app/controllers/api/v1/accounts/captain/scenarios_controller.rb @@ -0,0 +1,71 @@ +module Api + module V1 + module Accounts + module Captain + class ScenariosController < Api::V1::Accounts::BaseController + before_action :fetch_assistant + before_action :fetch_scenario, only: [:show, :update, :destroy] + + def index + @scenarios = @assistant.captain_scenarios.order(created_at: :desc) + render json: @scenarios + end + + def show + render json: @scenario + end + + def create + @scenario = @assistant.captain_scenarios.new(scenario_params) + @scenario.account = current_account + if @scenario.save + render json: @scenario + else + render_error_response(@scenario) + end + end + + def update + if @scenario.update(scenario_params) + render json: @scenario + else + render_error_response(@scenario) + end + end + + def destroy + @scenario.destroy + head :ok + end + + def suggest_triggers + # TODO: Implement AI suggestion logic + # For now, return a dummy list based on title/instruction if possible, or empty + render json: { keywords: 'keyword1, keyword2' } + end + + private + + def fetch_assistant + @assistant = current_account.captain_assistants.find(params[:assistant_id]) + end + + def fetch_scenario + @scenario = @assistant.captain_scenarios.find(params[:id]) + end + + def scenario_params + params.permit( + :title, + :description, + :instruction, + :trigger_keywords, + :enabled, + tools: [] + ) + end + end + end + end + end +end diff --git a/app/controllers/api/v1/accounts/captain/tools_controller.rb b/app/controllers/api/v1/accounts/captain/tools_controller.rb new file mode 100644 index 0000000..b5ad0d7 --- /dev/null +++ b/app/controllers/api/v1/accounts/captain/tools_controller.rb @@ -0,0 +1,73 @@ +module Api + module V1 + module Accounts + module Captain + class ToolsController < Api::V1::Accounts::BaseController + before_action :fetch_assistant + + NATIVE_TOOLS = [ + { key: 'react_to_message', name: 'React to Message', description: 'Reage a mensagens do usuário com emojis adequados.' }, + { key: 'check_availability', name: 'Check Availability', description: 'Verifica a disponibilidade de quartos e datas.' }, + { key: 'update_contact', name: 'Update Contact', description: 'Atualiza informações do contato (nome, email, telefone).' }, + { key: 'create_reservation_intent', name: 'Create Reservation Intent', description: 'Cria uma intenção de reserva e calcula valores.' }, + { key: 'generate_pix', name: 'Generate Pix', description: 'Gera código Pix Copy & Paste e QR Code.' }, + { key: 'list_reservations', name: 'List Reservations', description: 'Lista reservas anteriores do cliente.' }, + { key: 'status_suites', name: 'Status Suites', description: 'Verifica o status atual de ocupação das suítes.' }, + { key: 'suite_watchdog', name: 'Suite Watchdog', description: 'Monitoramento automático de status de suítes.' } + ] + + def index + tools = NATIVE_TOOLS.map do |tool| + config = @assistant.captain_tool_configs.find_by(tool_key: tool[:key]) + tool.merge( + enabled: config&.is_enabled.nil? || config.is_enabled, + webhook_url: config&.webhook_url, + plug_play_id: config&.plug_play_id, + plug_play_token: config&.plug_play_token, + fallback_message: config&.fallback_message + ) + end + render json: tools + end + + def update + tool_key = params[:id] + config = @assistant.captain_tool_configs.find_or_initialize_by(tool_key: tool_key) + + # Ensure context unique constraint is respected + config.account = current_account + + # Map 'enabled' from frontend to 'is_enabled' in DB + update_params = tool_params + update_params[:is_enabled] = update_params.delete(:enabled) if update_params.key?(:enabled) + + config.assign_attributes(update_params) + + if config.save + render json: config + else + render_error_response(config) + end + end + + private + + def fetch_assistant + @assistant = current_account.captain_assistants.find(params[:assistant_id]) + end + + def tool_params + params.require(:tool).permit( + :enabled, + :is_enabled, + :webhook_url, + :plug_play_id, + :plug_play_token, + :fallback_message + ) + end + end + end + end + end +end diff --git a/app/javascript/dashboard/components-next/captain/PageLayout.vue b/app/javascript/dashboard/components-next/captain/PageLayout.vue index 8873770..1ab9c6f 100755 --- a/app/javascript/dashboard/components-next/captain/PageLayout.vue +++ b/app/javascript/dashboard/components-next/captain/PageLayout.vue @@ -70,6 +70,10 @@ const props = defineProps({ type: Boolean, default: true, }, + isFullWidth: { + type: Boolean, + default: false, + }, }); const emit = defineEmits(['click', 'close', 'update:currentPage']); @@ -122,7 +126,10 @@ const handleCreateAssistant = () => {
-
+
@@ -213,7 +220,10 @@ const handleCreateAssistant = () => {
-
+
-import { computed, reactive, ref } from 'vue'; +/* eslint-disable @intlify/vue-i18n/no-raw-text, vue/no-bare-strings-in-template, @intlify/vue-i18n/no-dynamic-keys */ +import { computed, reactive, ref, watch } from 'vue'; import { useI18n } from 'vue-i18n'; import { useToggle } from '@vueuse/core'; import { useVuelidate } from '@vuelidate/core'; @@ -16,6 +17,26 @@ import TagMultiSelectComboBox from 'dashboard/components-next/combobox/TagMultiS import ScenariosAPI from 'dashboard/api/captain/scenarios'; import { useRoute } from 'vue-router'; import { useAlert } from 'dashboard/composables'; +import { SCENARIO_TEMPLATES } from 'dashboard/routes/dashboard/jasmine/data/templates'; + +const props = defineProps({ + triggerLabel: { + type: String, + default: 'CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.CREATE', + }, + startWithTemplates: { + type: Boolean, + default: false, + }, + triggerIcon: { + type: String, + default: '', + }, + triggerFaded: { + type: Boolean, + default: false, + }, +}); const emit = defineEmits(['add']); @@ -35,6 +56,13 @@ const state = reactive({ const allTools = useMapGetter('captainTools/getRecords'); const route = useRoute(); const isSuggesting = ref(false); +const showTemplateSelect = ref(false); + +watch(showPopover, val => { + if (val && props.startWithTemplates) { + showTemplateSelect.value = true; + } +}); const toolOptions = computed(() => { return allTools.value.map(tool => ({ @@ -113,7 +141,6 @@ const onSuggestTriggers = async () => { }); if (response.data.keywords) { - // Append if already exists, or replace? Replace feels safer for "suggestion" state.trigger_keywords = response.data.keywords; useAlert( t( @@ -127,16 +154,26 @@ const onSuggestTriggers = async () => { isSuggesting.value = false; } }; + +const applyTemplate = template => { + state.title = template.title; + state.description = template.description; + state.instruction = template.instruction; + state.trigger_keywords = template.trigger_keywords; + showTemplateSelect.value = false; +};