mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-11-03 20:48:07 +00:00 
			
		
		
		
	# Pull Request Template ## Description This PR fixes RTL alignment issues in the new conversation form, removes the unused [`form-checkbox`](https://github.com/chatwoot/chatwoot/pull/12151#discussion_r2266333315) class name and drops the `app-rtl--wrapper` class, which was previously used for RTL detection in `rtl.scss` (removed earlier) Fixes https://linear.app/chatwoot/issue/CW-5410/rtl-issues ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? ### Screenshots <img width="868" height="474" alt="image" src="https://github.com/user-attachments/assets/45995652-2895-49d5-a651-179090c949ec" /> <img width="868" height="656" alt="image" src="https://github.com/user-attachments/assets/a1cb4415-3fd4-4c9a-bc46-5e07e437d757" /> <img width="868" height="656" alt="image" src="https://github.com/user-attachments/assets/77c8981f-364e-4bf0-bea8-a4c42a76d065" /> ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [ ] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules
		
			
				
	
	
		
			334 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
			
		
		
	
	
			334 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
<script setup>
 | 
						|
import { reactive, computed, watch } from 'vue';
 | 
						|
import { useI18n } from 'vue-i18n';
 | 
						|
import { useVuelidate } from '@vuelidate/core';
 | 
						|
import { required, minLength } from '@vuelidate/validators';
 | 
						|
import { useMapGetter } from 'dashboard/composables/store';
 | 
						|
 | 
						|
import Input from 'dashboard/components-next/input/Input.vue';
 | 
						|
import Button from 'dashboard/components-next/button/Button.vue';
 | 
						|
import Editor from 'dashboard/components-next/Editor/Editor.vue';
 | 
						|
import Accordion from 'dashboard/components-next/Accordion/Accordion.vue';
 | 
						|
 | 
						|
const props = defineProps({
 | 
						|
  mode: {
 | 
						|
    type: String,
 | 
						|
    required: true,
 | 
						|
    validator: value => ['edit', 'create'].includes(value),
 | 
						|
  },
 | 
						|
  assistant: {
 | 
						|
    type: Object,
 | 
						|
    default: () => ({}),
 | 
						|
  },
 | 
						|
});
 | 
						|
 | 
						|
const emit = defineEmits(['submit']);
 | 
						|
 | 
						|
const { t } = useI18n();
 | 
						|
 | 
						|
const formState = {
 | 
						|
  uiFlags: useMapGetter('captainAssistants/getUIFlags'),
 | 
						|
};
 | 
						|
 | 
						|
const initialState = {
 | 
						|
  name: '',
 | 
						|
  description: '',
 | 
						|
  productName: '',
 | 
						|
  welcomeMessage: '',
 | 
						|
  handoffMessage: '',
 | 
						|
  resolutionMessage: '',
 | 
						|
  instructions: '',
 | 
						|
  features: {
 | 
						|
    conversationFaqs: false,
 | 
						|
    memories: false,
 | 
						|
    citations: false,
 | 
						|
  },
 | 
						|
  temperature: 1,
 | 
						|
};
 | 
						|
 | 
						|
const state = reactive({ ...initialState });
 | 
						|
 | 
						|
const validationRules = {
 | 
						|
  name: { required, minLength: minLength(1) },
 | 
						|
  description: { required, minLength: minLength(1) },
 | 
						|
  productName: { required, minLength: minLength(1) },
 | 
						|
  welcomeMessage: { minLength: minLength(1) },
 | 
						|
  handoffMessage: { minLength: minLength(1) },
 | 
						|
  resolutionMessage: { minLength: minLength(1) },
 | 
						|
  instructions: { minLength: minLength(1) },
 | 
						|
};
 | 
						|
 | 
						|
const v$ = useVuelidate(validationRules, state);
 | 
						|
 | 
						|
const isLoading = computed(() => formState.uiFlags.value.creatingItem);
 | 
						|
 | 
						|
const getErrorMessage = field => {
 | 
						|
  return v$.value[field].$error ? v$.value[field].$errors[0].$message : '';
 | 
						|
};
 | 
						|
 | 
						|
const formErrors = computed(() => ({
 | 
						|
  name: getErrorMessage('name'),
 | 
						|
  description: getErrorMessage('description'),
 | 
						|
  productName: getErrorMessage('productName'),
 | 
						|
  welcomeMessage: getErrorMessage('welcomeMessage'),
 | 
						|
  handoffMessage: getErrorMessage('handoffMessage'),
 | 
						|
  resolutionMessage: getErrorMessage('resolutionMessage'),
 | 
						|
  instructions: getErrorMessage('instructions'),
 | 
						|
}));
 | 
						|
 | 
						|
const updateStateFromAssistant = assistant => {
 | 
						|
  const { config = {} } = assistant;
 | 
						|
  state.name = assistant.name;
 | 
						|
  state.description = assistant.description;
 | 
						|
  state.productName = config.product_name;
 | 
						|
  state.welcomeMessage = config.welcome_message;
 | 
						|
  state.handoffMessage = config.handoff_message;
 | 
						|
  state.resolutionMessage = config.resolution_message;
 | 
						|
  state.instructions = config.instructions;
 | 
						|
  state.features = {
 | 
						|
    conversationFaqs: config.feature_faq || false,
 | 
						|
    memories: config.feature_memory || false,
 | 
						|
    citations: config.feature_citation || false,
 | 
						|
  };
 | 
						|
  state.temperature = config.temperature || 1;
 | 
						|
};
 | 
						|
 | 
						|
const handleBasicInfoUpdate = async () => {
 | 
						|
  const result = await Promise.all([
 | 
						|
    v$.value.name.$validate(),
 | 
						|
    v$.value.description.$validate(),
 | 
						|
    v$.value.productName.$validate(),
 | 
						|
  ]).then(results => results.every(Boolean));
 | 
						|
  if (!result) return;
 | 
						|
 | 
						|
  const payload = {
 | 
						|
    name: state.name,
 | 
						|
    description: state.description,
 | 
						|
    config: {
 | 
						|
      ...props.assistant.config,
 | 
						|
      product_name: state.productName,
 | 
						|
    },
 | 
						|
  };
 | 
						|
 | 
						|
  emit('submit', payload);
 | 
						|
};
 | 
						|
 | 
						|
const handleSystemMessagesUpdate = async () => {
 | 
						|
  const result = await Promise.all([
 | 
						|
    v$.value.welcomeMessage.$validate(),
 | 
						|
    v$.value.handoffMessage.$validate(),
 | 
						|
    v$.value.resolutionMessage.$validate(),
 | 
						|
  ]).then(results => results.every(Boolean));
 | 
						|
  if (!result) return;
 | 
						|
 | 
						|
  const payload = {
 | 
						|
    config: {
 | 
						|
      ...props.assistant.config,
 | 
						|
      welcome_message: state.welcomeMessage,
 | 
						|
      handoff_message: state.handoffMessage,
 | 
						|
      resolution_message: state.resolutionMessage,
 | 
						|
    },
 | 
						|
  };
 | 
						|
 | 
						|
  emit('submit', payload);
 | 
						|
};
 | 
						|
 | 
						|
const handleInstructionsUpdate = async () => {
 | 
						|
  const result = await v$.value.instructions.$validate();
 | 
						|
  if (!result) return;
 | 
						|
 | 
						|
  const payload = {
 | 
						|
    config: {
 | 
						|
      ...props.assistant.config,
 | 
						|
      temperature: state.temperature || 1,
 | 
						|
      instructions: state.instructions,
 | 
						|
    },
 | 
						|
  };
 | 
						|
 | 
						|
  emit('submit', payload);
 | 
						|
};
 | 
						|
 | 
						|
const handleFeaturesUpdate = () => {
 | 
						|
  const payload = {
 | 
						|
    config: {
 | 
						|
      ...props.assistant.config,
 | 
						|
      feature_faq: state.features.conversationFaqs,
 | 
						|
      feature_memory: state.features.memories,
 | 
						|
      feature_citation: state.features.citations,
 | 
						|
    },
 | 
						|
  };
 | 
						|
 | 
						|
  emit('submit', payload);
 | 
						|
};
 | 
						|
 | 
						|
watch(
 | 
						|
  () => props.assistant,
 | 
						|
  newAssistant => {
 | 
						|
    if (props.mode === 'edit' && newAssistant) {
 | 
						|
      updateStateFromAssistant(newAssistant);
 | 
						|
    }
 | 
						|
  },
 | 
						|
  { immediate: true }
 | 
						|
);
 | 
						|
</script>
 | 
						|
 | 
						|
<template>
 | 
						|
  <form class="flex flex-col gap-4" @submit.prevent="handleSubmit">
 | 
						|
    <!-- Basic Information Section -->
 | 
						|
    <Accordion
 | 
						|
      :title="t('CAPTAIN.ASSISTANTS.FORM.SECTIONS.BASIC_INFO')"
 | 
						|
      is-open
 | 
						|
    >
 | 
						|
      <div class="flex flex-col gap-4 pt-4">
 | 
						|
        <Input
 | 
						|
          v-model="state.name"
 | 
						|
          :label="t('CAPTAIN.ASSISTANTS.FORM.NAME.LABEL')"
 | 
						|
          :placeholder="t('CAPTAIN.ASSISTANTS.FORM.NAME.PLACEHOLDER')"
 | 
						|
          :message="formErrors.name"
 | 
						|
          :message-type="formErrors.name ? 'error' : 'info'"
 | 
						|
        />
 | 
						|
 | 
						|
        <Editor
 | 
						|
          v-model="state.description"
 | 
						|
          :label="t('CAPTAIN.ASSISTANTS.FORM.DESCRIPTION.LABEL')"
 | 
						|
          :placeholder="t('CAPTAIN.ASSISTANTS.FORM.DESCRIPTION.PLACEHOLDER')"
 | 
						|
          :message="formErrors.description"
 | 
						|
          :message-type="formErrors.description ? 'error' : 'info'"
 | 
						|
        />
 | 
						|
 | 
						|
        <Input
 | 
						|
          v-model="state.productName"
 | 
						|
          :label="t('CAPTAIN.ASSISTANTS.FORM.PRODUCT_NAME.LABEL')"
 | 
						|
          :placeholder="t('CAPTAIN.ASSISTANTS.FORM.PRODUCT_NAME.PLACEHOLDER')"
 | 
						|
          :message="formErrors.productName"
 | 
						|
          :message-type="formErrors.productName ? 'error' : 'info'"
 | 
						|
        />
 | 
						|
 | 
						|
        <div class="flex justify-end">
 | 
						|
          <Button
 | 
						|
            size="small"
 | 
						|
            :loading="isLoading"
 | 
						|
            @click="handleBasicInfoUpdate"
 | 
						|
          >
 | 
						|
            {{ t('CAPTAIN.ASSISTANTS.FORM.UPDATE') }}
 | 
						|
          </Button>
 | 
						|
        </div>
 | 
						|
      </div>
 | 
						|
    </Accordion>
 | 
						|
 | 
						|
    <!-- Instructions Section -->
 | 
						|
    <Accordion :title="t('CAPTAIN.ASSISTANTS.FORM.SECTIONS.INSTRUCTIONS')">
 | 
						|
      <div class="flex flex-col gap-4">
 | 
						|
        <Editor
 | 
						|
          v-model="state.instructions"
 | 
						|
          :placeholder="t('CAPTAIN.ASSISTANTS.FORM.INSTRUCTIONS.PLACEHOLDER')"
 | 
						|
          :message="formErrors.instructions"
 | 
						|
          :max-length="20000"
 | 
						|
          :message-type="formErrors.instructions ? 'error' : 'info'"
 | 
						|
        />
 | 
						|
 | 
						|
        <div class="flex flex-col gap-2 mt-4">
 | 
						|
          <label class="text-sm font-medium text-n-slate-12">
 | 
						|
            {{ t('CAPTAIN.ASSISTANTS.FORM.TEMPERATURE.LABEL') }}
 | 
						|
          </label>
 | 
						|
          <div class="flex items-center gap-4">
 | 
						|
            <input
 | 
						|
              v-model="state.temperature"
 | 
						|
              type="range"
 | 
						|
              min="0"
 | 
						|
              max="1"
 | 
						|
              step="0.1"
 | 
						|
              class="w-full"
 | 
						|
            />
 | 
						|
            <span class="text-sm text-n-slate-12">{{ state.temperature }}</span>
 | 
						|
          </div>
 | 
						|
          <p class="text-sm text-n-slate-11 italic">
 | 
						|
            {{ t('CAPTAIN.ASSISTANTS.FORM.TEMPERATURE.DESCRIPTION') }}
 | 
						|
          </p>
 | 
						|
        </div>
 | 
						|
        <div class="flex justify-end">
 | 
						|
          <Button
 | 
						|
            size="small"
 | 
						|
            :loading="isLoading"
 | 
						|
            :label="t('CAPTAIN.ASSISTANTS.FORM.UPDATE')"
 | 
						|
            @click="handleInstructionsUpdate"
 | 
						|
          />
 | 
						|
        </div>
 | 
						|
      </div>
 | 
						|
    </Accordion>
 | 
						|
 | 
						|
    <!-- Greeting Messages Section -->
 | 
						|
    <Accordion :title="t('CAPTAIN.ASSISTANTS.FORM.SECTIONS.SYSTEM_MESSAGES')">
 | 
						|
      <div class="flex flex-col gap-4 pt-4">
 | 
						|
        <Editor
 | 
						|
          v-model="state.handoffMessage"
 | 
						|
          :label="t('CAPTAIN.ASSISTANTS.FORM.HANDOFF_MESSAGE.LABEL')"
 | 
						|
          :placeholder="
 | 
						|
            t('CAPTAIN.ASSISTANTS.FORM.HANDOFF_MESSAGE.PLACEHOLDER')
 | 
						|
          "
 | 
						|
          :message="formErrors.handoffMessage"
 | 
						|
          :message-type="formErrors.handoffMessage ? 'error' : 'info'"
 | 
						|
        />
 | 
						|
 | 
						|
        <Editor
 | 
						|
          v-model="state.resolutionMessage"
 | 
						|
          :label="t('CAPTAIN.ASSISTANTS.FORM.RESOLUTION_MESSAGE.LABEL')"
 | 
						|
          :placeholder="
 | 
						|
            t('CAPTAIN.ASSISTANTS.FORM.RESOLUTION_MESSAGE.PLACEHOLDER')
 | 
						|
          "
 | 
						|
          :message="formErrors.resolutionMessage"
 | 
						|
          :message-type="formErrors.resolutionMessage ? 'error' : 'info'"
 | 
						|
        />
 | 
						|
 | 
						|
        <div class="flex justify-end">
 | 
						|
          <Button
 | 
						|
            size="small"
 | 
						|
            :loading="isLoading"
 | 
						|
            :label="t('CAPTAIN.ASSISTANTS.FORM.UPDATE')"
 | 
						|
            @click="handleSystemMessagesUpdate"
 | 
						|
          />
 | 
						|
        </div>
 | 
						|
      </div>
 | 
						|
    </Accordion>
 | 
						|
 | 
						|
    <!-- Features Section -->
 | 
						|
    <Accordion :title="t('CAPTAIN.ASSISTANTS.FORM.SECTIONS.FEATURES')">
 | 
						|
      <div class="flex flex-col gap-4 pt-4">
 | 
						|
        <div class="flex flex-col gap-2">
 | 
						|
          <label class="text-sm font-medium text-n-slate-12">
 | 
						|
            {{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.TITLE') }}
 | 
						|
          </label>
 | 
						|
          <div class="flex flex-col gap-2">
 | 
						|
            <label class="flex items-center gap-2">
 | 
						|
              <input
 | 
						|
                v-model="state.features.conversationFaqs"
 | 
						|
                type="checkbox"
 | 
						|
              />
 | 
						|
              {{
 | 
						|
                t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_CONVERSATION_FAQS')
 | 
						|
              }}
 | 
						|
            </label>
 | 
						|
            <label class="flex items-center gap-2">
 | 
						|
              <input v-model="state.features.memories" type="checkbox" />
 | 
						|
              {{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_MEMORIES') }}
 | 
						|
            </label>
 | 
						|
            <label class="flex items-center gap-2">
 | 
						|
              <input v-model="state.features.citations" type="checkbox" />
 | 
						|
              {{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_CITATIONS') }}
 | 
						|
            </label>
 | 
						|
          </div>
 | 
						|
        </div>
 | 
						|
 | 
						|
        <div class="flex justify-end">
 | 
						|
          <Button
 | 
						|
            size="small"
 | 
						|
            :loading="isLoading"
 | 
						|
            :label="t('CAPTAIN.ASSISTANTS.FORM.UPDATE')"
 | 
						|
            @click="handleFeaturesUpdate"
 | 
						|
          />
 | 
						|
        </div>
 | 
						|
      </div>
 | 
						|
    </Accordion>
 | 
						|
  </form>
 | 
						|
</template>
 |