diff --git a/.env.example b/.env.example index c8bac09ef..3e86c0c7e 100644 --- a/.env.example +++ b/.env.example @@ -107,3 +107,8 @@ RESERVA_1001_API_TOKEN= # Reserva Rede 1001 — URL base do app publico (usada pela Jasmine pra gerar links prefill) RESERVA_1001_BASE_URL=http://localhost:5180 + +# Reserva Rede 1001 — Supabase credentials para consultas de catalogo (preco, unidade) +RESERVA_1001_SUPABASE_URL= +RESERVA_1001_SUPABASE_ANON_KEY= +RESERVA_1001_SUPABASE_SCHEMA=reserva_hotel diff --git a/.gitignore b/.gitignore index 7c9d7fd29..1ed7d578e 100644 --- a/.gitignore +++ b/.gitignore @@ -129,3 +129,10 @@ Thumbs.db .env.aios .env.backup* reference/chatwoot-develop + +# Credentials / secrets — NUNCA commitar +docs/acessos_vps.md +docs/acessos*.md +**/acessos_vps* +**/*_secrets.md +**/*.credentials diff --git a/AGENTS.md b/AGENTS.md index 92a16bdd3..27c69591f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,56 +4,49 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -This is a **Chatwoot** customer engagement platform (open-source alternative to Intercom/Zendesk), customized for **fazer.ai**. It includes the **Synkra AIOS** framework overlay for AI-orchestrated development workflows. +**Chatwoot** customizado para **fazer.ai** — plataforma de atendimento ao cliente multi-canal com IA (Captain). Multi-tenant SaaS para hotelaria, com integração de PIX/Inter, reservas e WhatsApp. **Tech Stack:** - Backend: Ruby 3.4.4 + Rails 7.1 -- Frontend: Vue 3 + Vite -- Database: PostgreSQL with pgvector -- Background Jobs: Sidekiq -- Package Manager: **pnpm** (required, not npm/yarn) +- Frontend: Vue 3 + Vite + Pinia +- Database: PostgreSQL + pgvector +- Background Jobs: Sidekiq (com sidekiq-cron) +- Package Manager: **pnpm** (obrigatório — nunca npm/yarn) - Testing: RSpec (backend), Vitest (frontend) +- Event system: Wisper (pub/sub) +- Authorization: Pundit ## Development Commands -### Starting the Application +### Iniciar aplicação ```bash -# Development server (Rails backend + Sidekiq + Vite) -pnpm run dev - -# Individual processes: -# - Rails backend: http://localhost:3001 -# - Sidekiq: background worker -# - Vite: frontend dev server +pnpm run dev # overmind: Rails :3000 + Sidekiq + Vite +pnpm run start:dev # foreman (alternativo) ``` -### Testing +### Testes ```bash -# Frontend (Vitest) - CRITICAL: NO -- flag with pnpm test! -pnpm test # Run all tests -pnpm test # Run specific file (NOT pnpm test -- ) -pnpm test:watch # Watch mode -pnpm test:coverage # Coverage report +# Frontend (Vitest) — CRÍTICO: sem -- com pnpm! +pnpm test # todos +pnpm test app/javascript/path # arquivo específico (NÃO use pnpm test -- ) +pnpm test:watch +pnpm test:coverage # Backend (RSpec) -bundle exec rspec # All specs -bundle exec rspec spec/models/user_spec.rb # Specific file -bundle exec rspec spec/models/user_spec.rb:42 # Specific line +bundle exec rspec # todos +bundle exec rspec spec/models/contact_spec.rb # arquivo +bundle exec rspec spec/models/contact_spec.rb:42 # linha específica ``` -### Code Quality +### Qualidade de código ```bash -# JavaScript/Vue linting -pnpm run eslint # Check -pnpm run eslint:fix # Auto-fix - -# Ruby linting -bundle exec rubocop # Check -bundle exec rubocop -a # Auto-fix -pnpm run ruby:prettier # Same as rubocop -a +pnpm run eslint # lint JS/Vue +pnpm run eslint:fix # auto-fix +bundle exec rubocop # lint Ruby +bundle exec rubocop -a # auto-fix Ruby ``` ### Database @@ -61,72 +54,102 @@ pnpm run ruby:prettier # Same as rubocop -a ```bash bin/rails db:migrate bin/rails db:rollback -bin/rails db:reset -bin/rails db:seed +bin/rails db:reset && bin/rails db:seed ``` -## Architecture Overview +### i18n -### Backend Structure - -``` -app/ -├── controllers/ # API endpoints (API::V1::Accounts::*) -├── models/ # ActiveRecord models -├── services/ # Business logic (Whatsapp::Providers::*, etc.) -├── jobs/ # Sidekiq background jobs -├── listeners/ # Wisper event subscribers (pub/sub) -├── builders/ # Complex object construction -├── finders/ # Query objects -├── policies/ # Pundit authorization -└── javascript/ # Vue.js frontend - -enterprise/app/ # Enterprise features (Captain AI, billing) +```bash +pnpm run sync:i18n # sincroniza arquivo de tradução ``` -**Key Patterns:** -- **Services:** Business logic extracted from models -- **Builders:** Construct complex objects -- **Finders:** Encapsulate complex queries -- **Listeners:** Event-driven using Wisper -- **Policies:** Pundit for authorization -- **Jobs:** All async work in Sidekiq +## Arquitetura Backend -### Frontend Structure +### Modelo de dados central + +``` +Account (tenant raiz) + ├── Inbox (canal: WhatsApp, Email, Facebook, Instagram, Twilio...) + │ └── Contact (cliente) via ContactInbox + ├── Conversation (central: status, priority, SLA, custom_attributes) + │ ├── Message (conteúdo, attachments, sender) + │ ├── Agent (assignee) + │ └── Label + ├── AutomationRule + ├── Campaign + └── Article (help center) +``` + +### Padrões Rails usados + +| Padrão | Onde | Função | +|--------|------|--------| +| **Services** | `app/services/` | Toda lógica de negócio fora dos models | +| **Builders** | `app/builders/` | Construção de objetos complexos (ex: criar conversa + contato) | +| **Finders** | `app/finders/` | Query objects encapsulados | +| **Listeners** | `app/listeners/` | Subscribers Wisper para eventos de domínio | +| **Policies** | `app/policies/` | Autorização Pundit por recurso | +| **Jobs** | `app/jobs/` | Todo trabalho assíncrono via Sidekiq | + +### Estrutura de controllers + +``` +app/controllers/ +├── api/v1/accounts/{account_id}/ # Endpoints principais (Conversations, Contacts, Inboxes...) +├── api/v1/widget/ # Chat widget público +└── enterprise/api/v1/accounts/captain/ # Captain AI (enterprise) +``` + +### Enterprise — Captain AI (`enterprise/app/`) + +Camada fazer.ai sobre o Chatwoot base: + +- **Models chave:** `Captain::Unit` (multi-unidade hoteleira), `Captain::Assistant`, `Captain::Reservation`, `Captain::PixCharge`, `Captain::Document`, `Captain::ConversationInsight` +- **Integrações:** Inter API (pagamento PIX), WhatsApp, sincronização de reservas, webhooks +- **AI features:** LLM (OpenAI), copilot, audio transcription, label suggestion, help center search +- **Feature flags por account:** `captain_features` (Editor, Assistant, Copilot, LabelSuggestion, AudioTranscription, HelpCenterSearch) + +## Arquitetura Frontend ``` app/javascript/ -├── dashboard/ # Agent dashboard (Vue 3 + Vue Router + Vuex) -│ ├── routes/ # Page components -│ ├── store/ # Vuex state -│ ├── components/ # Reusable components -│ ├── api/ # API clients -│ └── i18n/ # Translations (en, pt_BR required!) -├── widget/ # Customer chat widget -├── sdk/ # Embeddable JavaScript SDK -├── portal/ # Public help center -└── shared/ # Shared utilities +├── dashboard/ # Dashboard do agente (Vue 3 + Vue Router + Pinia) +│ ├── routes/ # Componentes de página +│ ├── store/ # Pinia stores (55+ módulos: conversations, contacts, captain*) +│ ├── components/ # Componentes reutilizáveis +│ ├── api/ # Clientes HTTP por recurso +│ └── i18n/locale/ # Traduções (en + pt_BR SEMPRE) +├── widget/ # Widget de chat embeddable +├── sdk/ # SDK JS (build separado: pnpm run build:sdk) +├── portal/ # Help center público +└── shared/ # Utilities compartilhados ``` -**Vite Import Aliases:** +**Aliases Vite:** - `components` → `app/javascript/dashboard/components` - `dashboard` → `app/javascript/dashboard` - `helpers` → `app/javascript/shared/helpers` -- `shared`, `widget`, `survey`, `v3` → respective directories +- `shared`, `widget`, `survey`, `v3` → diretórios equivalentes -## Critical Conventions +**Bibliotecas chave:** ProseMirror (rich text), ActionCable (real-time), Chart.js, Twilio Voice SDK -### fazer.ai Branding -**ALWAYS** style as `fazer.ai` (lowercase with dot), **NEVER** `Fazer.ai` or `FAZER.AI` +## Convenções críticas -### Internationalization -**ALWAYS include pt_BR translations** for any new user-facing text -- Location: `app/javascript/dashboard/i18n/locale/{en,pt_BR}/` +### Branding +`fazer.ai` — sempre minúsculo com ponto. Nunca `Fazer.ai` ou `FAZER.AI`. -### Testing Philosophy -- Add specs when modifying code (use judgment) -- Test behavior, not implementation -- Consider cross-stack impacts (backend ↔ frontend) +### Internacionalização +Qualquer texto visível ao usuário **exige** tradução em `en` e `pt_BR`: +``` +app/javascript/dashboard/i18n/locale/en/ +app/javascript/dashboard/i18n/locale/pt_BR/ +``` + +### Novos canais / integrações +Siga o padrão existente em `app/services/whatsapp/` ou `app/services/instagram/` — nunca coloque lógica de canal no controller. + +### Background jobs +Toda operação demorada vai para Sidekiq. Jobs em `app/jobs/`, enterprise em `enterprise/app/jobs/`. --- @@ -146,9 +169,10 @@ This repository includes **Synkra AIOS** - an AI-orchestrated development system ## Quality Gates -- Rode `npm run lint` -- Rode `npm run typecheck` -- Rode `npm test` +- Rode `pnpm run eslint` +- Rode `bundle exec rubocop` +- Rode `pnpm test` +- Rode `bundle exec rspec` - Atualize checklist e file list da story antes de concluir diff --git a/app/controllers/api/v1/accounts/captain/reports/executive_controller.rb b/app/controllers/api/v1/accounts/captain/reports/executive_controller.rb new file mode 100644 index 000000000..c663b2edb --- /dev/null +++ b/app/controllers/api/v1/accounts/captain/reports/executive_controller.rb @@ -0,0 +1,113 @@ +# Fornece o CEO Digest (via Captain::Reports::CeoDigestService) para a UI +# + endpoint de drill-down (busca conversas que contêm um texto) + disparo on-demand +# do envio ao Mattermost. +class Api::V1::Accounts::Captain::Reports::ExecutiveController < Api::V1::Accounts::BaseController + # GET /api/v1/accounts/:account_id/captain/reports/executive + # Params: period_start, period_end + def show + period_end = parse_date(params[:period_end], Time.zone.today - 1) + period_start = parse_date(params[:period_start], period_end - 6.days) + + digest = Captain::Reports::CeoDigestService.new( + account: Current.account, + period_start: period_start, + period_end: period_end + ).call + + render json: digest + end + + # GET /api/v1/accounts/:account_id/captain/reports/executive/drilldown + # Params: query (texto a procurar), period_start, period_end, inbox_id (opcional) + # Retorna: conversas que contêm o texto, com link para abrir no Chatwoot. + def drilldown + query = params[:query].to_s.strip + return render json: { conversations: [] } if query.blank? + + period_end = parse_date(params[:period_end], Time.zone.today - 1) + period_start = parse_date(params[:period_start], period_end - 6.days) + inbox_id = params[:inbox_id].presence&.to_i + + conversations = search_conversations(query, period_start, period_end, inbox_id) + tokens = extract_tokens(query) + render json: { query: query, conversations: conversations, tokens: tokens } + end + + # POST /api/v1/accounts/:account_id/captain/reports/executive/deliver + # Dispara entrega do digest ao Mattermost agora (usa config da conta). + def deliver + period_end = parse_date(params[:period_end], Time.zone.today - 1) + period_start = parse_date(params[:period_start], period_end - 6.days) + + Captain::Reports::CeoDigestJob.perform_later(Current.account.id, period_start, period_end) + render json: { status: 'queued', period_start: period_start, period_end: period_end }, status: :accepted + end + + private + + def search_conversations(query, period_start, period_end, inbox_id) + tokens = extract_tokens(query) + return [] if tokens.empty? + + scope = Current.account.conversations + .where(created_at: period_start.beginning_of_day..period_end.end_of_day) + .joins(:messages) + scope = scope.where(inbox_id: inbox_id) if inbox_id + scope = apply_token_filter(scope, tokens) + scope.distinct.includes(:contact, :inbox).limit(10).map { |c| format_conversation(c) } + end + + # Quebra a descrição do insight em palavras relevantes (4+ chars, sem stopwords) + # e retorna só os tokens mais distintivos. Assim "Facilidade no acesso ao link + # de pagamento" vira ["facilidade", "acesso", "link", "pagamento"]. + STOPWORDS = %w[ + para com que uma esse essa esta isso aqui ali mais menos sobre entre + sem nao sim the and for that this with from into have need want + sobre quando onde como porque qual quais nesse nessa dessa desse dele + dela dos das pelo pela pelos pelas deste desta disto isto foi ser + ].freeze + + def extract_tokens(query) + query.to_s.downcase + .scan(/[[:alnum:]áéíóúâêôãõçàü]+/) + .reject { |w| w.length < 4 || STOPWORDS.include?(w) } + .uniq + .first(8) + end + + def apply_token_filter(scope, tokens) + conditions = Array.new(tokens.size, 'messages.content ILIKE ?').join(' OR ') + values = tokens.map { |t| "%#{sanitize_like(t)}%" } + scope.where(conditions, *values) + end + + def sanitize_like(str) + str.gsub('\\', '\\\\').gsub('%', '\\%').gsub('_', '\\_') + end + + def format_conversation(conv) + { + id: conv.display_id, + internal_id: conv.id, + status: conv.status, + inbox_id: conv.inbox_id, + inbox_name: conv.inbox&.name, + contact_name: conv.contact&.name, + created_at: conv.created_at, + updated_at: conv.updated_at, + url: conversation_url(conv) + } + end + + def conversation_url(conv) + "/app/accounts/#{Current.account.id}/conversations/#{conv.display_id}" + end + + def parse_date(param, default) + return default if param.blank? + + Date.parse(param.to_s) + rescue ArgumentError, TypeError + default + end +end diff --git a/app/controllers/api/v1/accounts/captain/reports/funnel_controller.rb b/app/controllers/api/v1/accounts/captain/reports/funnel_controller.rb new file mode 100644 index 000000000..b8a32d9f9 --- /dev/null +++ b/app/controllers/api/v1/accounts/captain/reports/funnel_controller.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class Api::V1::Accounts::Captain::Reports::FunnelController < Api::V1::Accounts::BaseController + before_action :current_account + before_action -> { check_authorization(Captain::Assistant) } + + def show + days = params[:period_days].to_i + report = Captain::Reports::ConversionFunnelService.new( + account: current_account, + period_days: days + ).call + render json: report + end +end diff --git a/app/controllers/api/v1/accounts/captain/reports/operational_controller.rb b/app/controllers/api/v1/accounts/captain/reports/operational_controller.rb index c790accdc..1a1f28869 100644 --- a/app/controllers/api/v1/accounts/captain/reports/operational_controller.rb +++ b/app/controllers/api/v1/accounts/captain/reports/operational_controller.rb @@ -2,30 +2,43 @@ class Api::V1::Accounts::Captain::Reports::OperationalController < Api::V1::Acco def show period_start = parse_date(params[:period_start], Time.zone.today.beginning_of_month) period_end = parse_date(params[:period_end], Time.zone.today) - unit = params[:unit_id].present? ? Current.account.captain_units.find_by(id: params[:unit_id]) : nil - - render json: build_operational_report(unit, period_start, period_end) + render json: build_operational_report(find_unit, find_inbox, period_start, period_end) end private + def find_unit + return nil if params[:unit_id].blank? + + Current.account.captain_units.find_by(id: params[:unit_id]) + end + + def find_inbox + return nil if params[:inbox_id].blank? + + Current.account.inboxes.find_by(id: params[:inbox_id]) + end + def parse_date(param, default) param.present? ? Date.parse(param) : default rescue ArgumentError default end - def build_operational_report(unit, period_start, period_end) - conversations = scoped_conversations(unit, period_start, period_end) + def build_operational_report(unit, inbox, period_start, period_end) + conversations = scoped_conversations(unit, inbox, period_start, period_end) { period: { start: period_start, end: period_end }, unit_id: unit&.id, unit_name: unit&.name, + inbox_id: inbox&.id, + inbox_name: inbox&.name, conversations: conversation_metrics(conversations), - reservations: reservation_metrics(unit, period_start, period_end), + reservations: reservation_metrics(unit, inbox, period_start, period_end), hourly_distribution: hourly_distribution(conversations), - daily_distribution: daily_distribution(conversations, period_start, period_end) + daily_distribution: daily_distribution(conversations, period_start, period_end), + by_inbox: inbox.nil? ? by_inbox_breakdown(conversations) : [] } end @@ -42,8 +55,8 @@ class Api::V1::Accounts::Captain::Reports::OperationalController < Api::V1::Acco } end - def reservation_metrics(unit, period_start, period_end) - reservations = scoped_reservations(unit, period_start, period_end) + def reservation_metrics(unit, inbox, period_start, period_end) + reservations = scoped_reservations(unit, inbox, period_start, period_end) paid = reservations.where(status: 'paid') expired = reservations.where(status: 'expired') @@ -73,21 +86,54 @@ class Api::V1::Accounts::Captain::Reports::OperationalController < Api::V1::Acco end end - def scoped_conversations(unit, period_start, period_end) + def scoped_conversations(unit, inbox, period_start, period_end) scope = Current.account.conversations.where(created_at: period_start.beginning_of_day..period_end.end_of_day) - if unit + if inbox + scope = scope.where(inbox_id: inbox.id) + elsif unit inbox_ids = unit.inboxes.pluck(:id) scope = scope.where(inbox_id: inbox_ids) if inbox_ids.any? end scope end - def scoped_reservations(unit, period_start, period_end) + def scoped_reservations(unit, inbox, period_start, period_end) scope = Current.account.captain_reservations.where(created_at: period_start.beginning_of_day..period_end.end_of_day) - scope = scope.where(captain_unit_id: unit.id) if unit + if inbox + conversation_ids = Current.account.conversations.where(inbox_id: inbox.id).pluck(:id) + scope = scope.where(conversation_id: conversation_ids) + elsif unit + scope = scope.where(captain_unit_id: unit.id) + end scope end + def by_inbox_breakdown(conversations) + resolved_int = Conversation.statuses['resolved'] + open_int = Conversation.statuses['open'] + inbox_data = conversations.group(:inbox_id).pluck( + :inbox_id, + Arel.sql('COUNT(*)'), + Arel.sql("COUNT(*) FILTER (WHERE status = #{resolved_int})"), + Arel.sql("COUNT(*) FILTER (WHERE status = #{open_int})") + ) + inbox_names = Current.account.inboxes.where(id: inbox_data.map(&:first)).pluck(:id, :name).to_h + + rows = inbox_data.map { |inbox_id, total, resolved, open| build_inbox_row(inbox_id, total, resolved, open, inbox_names) } + rows.sort_by { |row| -row[:total] } + end + + def build_inbox_row(inbox_id, total, resolved, open, inbox_names) + { + inbox_id: inbox_id, + inbox_name: inbox_names[inbox_id] || "Canal ##{inbox_id}", + total: total, + resolved: resolved, + open: open, + resolution_rate: safe_rate(resolved, total) + } + end + def avg_resolution_minutes(conversations) return 0 if conversations.none? diff --git a/app/controllers/public/api/v1/captain/roulette_notifications_controller.rb b/app/controllers/public/api/v1/captain/roulette_notifications_controller.rb new file mode 100644 index 000000000..f946622c2 --- /dev/null +++ b/app/controllers/public/api/v1/captain/roulette_notifications_controller.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# Endpoint público disparado pelo frontend (reserva-1001 /roleta/:token) assim que +# o prêmio é revelado. Só enfileira o job — todo o trabalho (validação, claim atômico, +# envio de msg) acontece em Captain::Roleta::NotifyRevealedJob. +class Public::Api::V1::Captain::RouletteNotificationsController < ActionController::API + def create + token = params[:token].to_s.strip + if token.blank? + render json: { error: 'token ausente' }, status: :bad_request + return + end + + Captain::Roleta::NotifyRevealedJob.perform_later(token) + render json: { enqueued: true }, status: :accepted + rescue StandardError => e + Rails.logger.error "[RouletteNotifications] erro: #{e.class} - #{e.message}" + render json: { error: 'Internal error' }, status: :unprocessable_entity + end +end diff --git a/app/javascript/dashboard/api/captain/funnel.js b/app/javascript/dashboard/api/captain/funnel.js new file mode 100644 index 000000000..ed48f4513 --- /dev/null +++ b/app/javascript/dashboard/api/captain/funnel.js @@ -0,0 +1,14 @@ +/* global axios */ +import ApiClient from '../ApiClient'; + +class CaptainFunnel extends ApiClient { + constructor() { + super('captain/reports/funnel', { accountScoped: true }); + } + + get(periodDays = 30) { + return axios.get(this.url, { params: { period_days: periodDays } }); + } +} + +export default new CaptainFunnel(); diff --git a/app/javascript/dashboard/api/captain/reports.js b/app/javascript/dashboard/api/captain/reports.js index b1866f04c..435aa7c3b 100644 --- a/app/javascript/dashboard/api/captain/reports.js +++ b/app/javascript/dashboard/api/captain/reports.js @@ -21,6 +21,18 @@ class CaptainReportsAPI extends ApiClient { generateInsight(data) { return axios.post(`${this.url}/insights/generate`, data); } + + getExecutive(params = {}) { + return axios.get(`${this.url}/executive`, { params }); + } + + drilldown(params = {}) { + return axios.get(`${this.url}/executive/drilldown`, { params }); + } + + deliverExecutive(params = {}) { + return axios.post(`${this.url}/executive/deliver`, params); + } } export default new CaptainReportsAPI(); diff --git a/app/javascript/dashboard/api/captain/reservations.js b/app/javascript/dashboard/api/captain/reservations.js index c2e616f94..7cc681640 100644 --- a/app/javascript/dashboard/api/captain/reservations.js +++ b/app/javascript/dashboard/api/captain/reservations.js @@ -25,6 +25,18 @@ class CaptainReservations extends ApiClient { pix(id) { return axios.get(`${this.url}/${id}/pix`); } + + cancel(id, reason = '') { + return axios.post(`${this.url}/${id}/cancel`, { reason }); + } + + markAsPaid(id, note = '') { + return axios.post(`${this.url}/${id}/mark_as_paid`, { note }); + } + + regeneratePix(id) { + return axios.post(`${this.url}/${id}/regenerate_pix`, {}); + } } export default new CaptainReservations(); diff --git a/app/javascript/dashboard/api/captain/roleta.js b/app/javascript/dashboard/api/captain/roleta.js new file mode 100644 index 000000000..325ed0b2b --- /dev/null +++ b/app/javascript/dashboard/api/captain/roleta.js @@ -0,0 +1,24 @@ +/* global axios */ +import ApiClient from '../ApiClient'; + +class CaptainRoleta extends ApiClient { + constructor() { + super('captain/roleta', { accountScoped: true }); + } + + pending(params = {}) { + return axios.get(`${this.url}/pending`, { params }); + } + + redeem(code, notes = '') { + return axios.post(`${this.url}/redeem`, { code, notes }); + } + + weeklyReport(periodDays = 7) { + return axios.get(`${this.url}/weekly_report`, { + params: { period_days: periodDays }, + }); + } +} + +export default new CaptainRoleta(); diff --git a/app/javascript/dashboard/components-next/captain/assistant/AddNewScenariosDialog.vue b/app/javascript/dashboard/components-next/captain/assistant/AddNewScenariosDialog.vue index 89f115a64..8f37fab2d 100644 --- a/app/javascript/dashboard/components-next/captain/assistant/AddNewScenariosDialog.vue +++ b/app/javascript/dashboard/components-next/captain/assistant/AddNewScenariosDialog.vue @@ -21,6 +21,7 @@ const state = reactive({ id: '', title: '', description: '', + trigger_keywords: '', instruction: '', }); @@ -55,6 +56,7 @@ const resetState = () => { id: '', title: '', description: '', + trigger_keywords: '', instruction: '', }); }; @@ -119,6 +121,24 @@ const onClickCancel = () => { :message-type="descriptionError ? 'error' : 'info'" show-character-count /> +