1576 lines
56 KiB
Vue
1576 lines
56 KiB
Vue
|
|
<script setup>
|
|||
|
|
import { ref, nextTick, computed, watch, onMounted } from "vue";
|
|||
|
|
import { useTheme } from "vuetify";
|
|||
|
|
import { themes } from "@/plugins/vuetify/theme";
|
|||
|
|
import { PerfectScrollbar } from "vue3-perfect-scrollbar";
|
|||
|
|
|
|||
|
|
// Constants
|
|||
|
|
const MAX_FILE_SIZE = 5 * 1024 * 1024;
|
|||
|
|
const MAX_FILES = 5;
|
|||
|
|
const ALLOWED_FILE_TYPES = [
|
|||
|
|
"image/jpeg", "image/png", "image/gif", "application/pdf",
|
|||
|
|
"application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|||
|
|
"text/plain"
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
const QUICK_REPLIES = [
|
|||
|
|
"Resource Management", "Task Management", "New Project"
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
const COMMANDS = ["/start", "/help", "/pricing", "/faq", "/cancel"];
|
|||
|
|
|
|||
|
|
const MODELS = ref([
|
|||
|
|
{ identifier: "model1", name: "ChatBot Basic", color: "primary", bgColor: "primary-lighten-5" },
|
|||
|
|
{ identifier: "model2", name: "ChatBot Pro", color: "success", bgColor: "success-lighten-5" },
|
|||
|
|
{ identifier: "model3", name: "ChatBot Advanced", color: "warning", bgColor: "warning-lighten-5" }
|
|||
|
|
]);
|
|||
|
|
|
|||
|
|
// Page configuration
|
|||
|
|
definePage({
|
|||
|
|
meta: {
|
|||
|
|
layoutWrapperClasses: "layout-content-height-fixed",
|
|||
|
|
layoutNavbar: false,
|
|||
|
|
hideFooter: true,
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Theme
|
|||
|
|
const { name } = useTheme();
|
|||
|
|
const chatContentContainerBg = computed(
|
|||
|
|
() => themes?.[name.value]?.colors?.background || "transparent"
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// Core state
|
|||
|
|
const messages = ref([]);
|
|||
|
|
const msg = ref("");
|
|||
|
|
const refInputEl = ref();
|
|||
|
|
const scrollEl = ref();
|
|||
|
|
const isComposing = ref(false);
|
|||
|
|
const isBotTyping = ref(false);
|
|||
|
|
const isRequestCancelled = ref(false);
|
|||
|
|
const abortController = ref(null);
|
|||
|
|
|
|||
|
|
// UI state
|
|||
|
|
const isInputCentered = ref(true);
|
|||
|
|
const showModelMenu = ref(false);
|
|||
|
|
const showProjectMenu = ref(false);
|
|||
|
|
const showCommandList = ref(false);
|
|||
|
|
const showSuggestions = ref(false);
|
|||
|
|
const showUndoPopup = ref(false);
|
|||
|
|
const selectedSuggestionIndex = ref(-1);
|
|||
|
|
const isWebSearchEnabled = ref(false);
|
|||
|
|
|
|||
|
|
// File handling
|
|||
|
|
const isUploadModalOpen = ref(false);
|
|||
|
|
const selectedAttachments = ref([]);
|
|||
|
|
const fileInputRef = ref(null);
|
|||
|
|
const fileError = ref("");
|
|||
|
|
|
|||
|
|
// Form management
|
|||
|
|
const activeForm = ref(null);
|
|||
|
|
const activeMultiForm = ref(null);
|
|||
|
|
const currentFormStep = ref(0);
|
|||
|
|
const formResponses = ref({});
|
|||
|
|
const multiFormData = ref({});
|
|||
|
|
const answeredForms = ref(new Set());
|
|||
|
|
|
|||
|
|
// Confirmation system
|
|||
|
|
const showConfirmation = ref(false);
|
|||
|
|
const showStepSelection = ref(false);
|
|||
|
|
const confirmationData = ref({});
|
|||
|
|
const availableSteps = ref([]);
|
|||
|
|
const pendingConfirmationFormId = ref(null);
|
|||
|
|
|
|||
|
|
// Project management
|
|||
|
|
const savedProjects = JSON.parse(localStorage.getItem("projectOptions") || "[]");
|
|||
|
|
const projectOptions = ref([...savedProjects, "+ New Project"]);
|
|||
|
|
const selectedProject = ref("");
|
|||
|
|
|
|||
|
|
// Model management
|
|||
|
|
const selectedModelIdentifier = ref(MODELS.value[0].identifier);
|
|||
|
|
|
|||
|
|
// Message queue
|
|||
|
|
const messageQueue = ref([]);
|
|||
|
|
const isProcessingQueue = ref(false);
|
|||
|
|
|
|||
|
|
// Suggestions
|
|||
|
|
const suggestions = ref([]);
|
|||
|
|
const filteredCommands = ref([]);
|
|||
|
|
|
|||
|
|
// Undo functionality
|
|||
|
|
const chatBackup = ref(null);
|
|||
|
|
const undoTimer = ref(null);
|
|||
|
|
const undoProgress = ref(100);
|
|||
|
|
const undoInterval = ref(null);
|
|||
|
|
|
|||
|
|
// Suggestion database
|
|||
|
|
const suggestionDatabase = {
|
|||
|
|
پروژه: ["New Project ایجاد کنم", "پروژه وب سایت", "پروژه اپلیکیشن موبایل", "پروژه سیستم مدیریت", "پروژه فروشگاه آنلاین", "پروژه های انجام شده", "پروژه در حال اجرا"],
|
|||
|
|
project: ["create a new project", "project management system", "project timeline", "project budget estimation", "project requirements", "project status update", "project completion"],
|
|||
|
|
قیمت: ["قیمت پروژه وب سایت", "قیمت اپلیکیشن موبایل", "قیمت سیستم مدیریت", "قیمت فروشگاه آنلاین", "قیمت گذاری پروژه ها", "قیمت نگهداری و پشتیبانی"],
|
|||
|
|
price: ["pricing for website", "pricing for mobile app", "pricing for management system", "pricing plans", "price calculator", "price comparison"],
|
|||
|
|
"وب سایت": ["وب سایت شرکتی", "وب سایت فروشگاهی", "وب سایت شخصی", "وب سایت خبری", "وب سایت آموزشی", "وب سایت رستوران"],
|
|||
|
|
website: ["website design", "website development", "website maintenance", "website optimization", "website hosting", "website security"],
|
|||
|
|
اپلیکیشن: ["اپلیکیشن موبایل", "اپلیکیشن اندروید", "اپلیکیشن آیفون", "اپلیکیشن تحت وب", "اپلیکیشن فروشگاهی", "اپلیکیشن بانکی"],
|
|||
|
|
app: ["mobile app development", "android app", "ios app", "web app", "app store optimization", "app maintenance"],
|
|||
|
|
پشتیبانی: ["پشتیبانی فنی", "پشتیبانی 24 ساعته", "پشتیبانی پروژه", "پشتیبانی تلفنی", "پشتیبانی آنلاین", "پشتیبانی و نگهداری"],
|
|||
|
|
support: ["technical support", "24/7 support", "customer support", "project support", "maintenance support", "support ticket"]
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Forms configuration
|
|||
|
|
const forms = {
|
|||
|
|
subscription: {
|
|||
|
|
id: "subscription", title: "Subscription Confirmation",
|
|||
|
|
question: "Would you like to subscribe to our newsletter?",
|
|||
|
|
options: ["Yes", "No"],
|
|||
|
|
callback: (response) => response === "Yes"
|
|||
|
|
? "Great! You've been subscribed to our newsletter. You'll receive updates on our latest features and promotions."
|
|||
|
|
: "No problem. You can always subscribe later if you change your mind."
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
feedback: {
|
|||
|
|
id: "feedback", title: "Service Feedback",
|
|||
|
|
question: "Was this information helpful?", options: ["Yes", "No"],
|
|||
|
|
callback: (response) => response === "Yes"
|
|||
|
|
? "Thank you for your positive feedback! We're glad we could help."
|
|||
|
|
: "We're sorry to hear that. How can we improve our service?"
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
demo: {
|
|||
|
|
id: "demo", title: "Demo Request",
|
|||
|
|
question: "Would you like to schedule a product demo?", options: ["Yes", "No"],
|
|||
|
|
callback: (response) => response === "Yes"
|
|||
|
|
? "Excellent! Please provide your preferred date and time, and our team will reach out to confirm your demo session."
|
|||
|
|
: "No problem. You can request a demo anytime you're ready to explore our product further."
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
newProject: {
|
|||
|
|
id: "newProject", title: "New Project", type: "multi-step", requiresConfirmation: true,
|
|||
|
|
steps: [
|
|||
|
|
{ id: "projectName", title: "Project Name", question: "Enter your project name:", type: "text", required: true },
|
|||
|
|
{ id: "projectType", title: "Project Type", question: "Select your project type:", type: "options", options: ["Website", "Mobile App", "Management System", "Online Store", "Other"] },
|
|||
|
|
{ id: "budget", title: "Project Budget", question: "What is the estimated budget for the project?", type: "options", options: ["Less than 10M", "10M–50M", "50M–100M", "More than 100M"] },
|
|||
|
|
{ id: "timeline", title: "Project Timeline", question: "What is the desired duration to complete the project?", type: "options", options: ["Less than 1 month", "1–3 months", "3–6 months", "More than 6 months"] },
|
|||
|
|
{ id: "priority", title: "Project Priority", question: "What is the priority level of this project?", type: "options", options: ["Low", "Medium", "High", "Urgent"] },
|
|||
|
|
{ id: "features", title: "Required Features", question: "Which features are important to you?", type: "options", options: ["User Authentication", "Admin Panel", "Online Payment", "Chat & Messaging", "Reporting", "API"] }
|
|||
|
|
],
|
|||
|
|
callback: (data) => {
|
|||
|
|
const summary = `✅ New project submitted!\n\n📋 Project Summary:\n• Name: ${data.projectName}\n• Type: ${data.projectType}\n• Budget: ${data.budget}\n• Timeline: ${data.timeline}\n• Priority: ${data.priority}\n• Features: ${Array.isArray(data.features) ? data.features.join(", ") : data.features}${data.description ? `\n• Notes: ${data.description}` : ""}`;
|
|||
|
|
return summary.trim();
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
resourceManagement: {
|
|||
|
|
id: "resourceManagement", title: "Resource Management", type: "multi-step", requiresConfirmation: false,
|
|||
|
|
steps: [
|
|||
|
|
{ id: "resourceType", title: "Resource Type", question: "What type of resource do you want to manage?", type: "options", options: ["Human Resources", "Financial Resources", "Material Resources", "Technology Resources", "Information Resources"] },
|
|||
|
|
{ id: "actionType", title: "Action Type", question: "What would you like to do?", type: "options", options: ["Analyze", "Update"] },
|
|||
|
|
{ id: "analysisType", title: "Analysis Type", question: "What type of analysis would you like to perform?", type: "options", options: ["General Analysis", "Custom Analysis"], condition: (data) => data.actionType === "Analyze" },
|
|||
|
|
{ id: "updateDetails", title: "Update Details", question: "Please provide details for the resource update:", type: "text", required: true, condition: (data) => data.actionType === "Update" }
|
|||
|
|
],
|
|||
|
|
callback: (data) => {
|
|||
|
|
if (data.actionType === "Analyze") {
|
|||
|
|
return `✅ Resource Analysis Complete!\n\n📊 Analysis Summary:\n• Resource Type: ${data.resourceType}\n• Management Scope: ${data.managementScope}\n• Analysis Type: ${data.analysisType}\n\n${data.analysisType === "General Analysis" ? "📋 General analysis has been performed on your selected resources." : "🔍 Custom analysis has been initiated based on your specific requirements."}`.trim();
|
|||
|
|
} else {
|
|||
|
|
return `✅ Resource Update Submitted!\n\n📝 Update Summary:\n• Resource Type: ${data.resourceType}\n• Management Scope: ${data.managementScope}\n• Update Details: ${data.updateDetails}\n\nYour resource update request has been processed successfully.`.trim();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Computed properties
|
|||
|
|
const isQuickRepliesVisible = computed(() => {
|
|||
|
|
if (activeMultiForm.value !== null) return false;
|
|||
|
|
if (activeForm.value !== null) return false;
|
|||
|
|
if (isConfirmationActive.value) return false;
|
|||
|
|
if (isWebSearchEnabled.value) return false;
|
|||
|
|
if (pendingConfirmationFormId.value !== null) return false;
|
|||
|
|
return true;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const isConfirmationActive = computed(() => showConfirmation.value || showStepSelection.value);
|
|||
|
|
|
|||
|
|
const isInputDisabled = computed(() => {
|
|||
|
|
if (isConfirmationActive.value) return false;
|
|||
|
|
if (activeForm.value) return true;
|
|||
|
|
if (activeMultiForm.value && getCurrentStepHasOptions()) return true;
|
|||
|
|
return false;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const activeModelColor = computed(() => {
|
|||
|
|
const selectedModel = MODELS.value.find(m => m.identifier === selectedModelIdentifier.value);
|
|||
|
|
return selectedModel ? selectedModel.color : "primary";
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const activeModelBgColor = computed(() => {
|
|||
|
|
const selectedModel = MODELS.value.find(m => m.identifier === selectedModelIdentifier.value);
|
|||
|
|
return selectedModel ? selectedModel.bgColor : "primary-lighten-5";
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const selectedModelName = computed(() => {
|
|||
|
|
if (!selectedModelIdentifier.value) return "Select Model";
|
|||
|
|
const model = MODELS.value.find(m => m.identifier === selectedModelIdentifier.value);
|
|||
|
|
return model ? model.name : "Select Model";
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const toggleWebSearch = () => {
|
|||
|
|
isWebSearchEnabled.value = !isWebSearchEnabled.value;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Utility functions
|
|||
|
|
const getCurrentStepHasOptions = () => {
|
|||
|
|
if (!activeMultiForm.value) return false;
|
|||
|
|
const form = forms[activeMultiForm.value];
|
|||
|
|
const currentStep = form.steps[currentFormStep.value];
|
|||
|
|
return currentStep.type === "options" || currentStep.type === "multiple";
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const cleanHtmlResponse = (htmlString) => {
|
|||
|
|
const parser = new DOMParser();
|
|||
|
|
const doc = parser.parseFromString(htmlString, 'text/html');
|
|||
|
|
return doc.body.textContent || doc.body.innerText || '';
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const scrollToBottom = async () => {
|
|||
|
|
await nextTick();
|
|||
|
|
const ps = scrollEl.value;
|
|||
|
|
if (!ps) return;
|
|||
|
|
|
|||
|
|
const el = ps.$el || ps;
|
|||
|
|
if (el) {
|
|||
|
|
el.scrollTop = el.scrollHeight;
|
|||
|
|
ps.update?.();
|
|||
|
|
setTimeout(() => { el.scrollTop = el.scrollHeight; }, 50);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const findNextValidStep = (form, currentStepIndex, formData) => {
|
|||
|
|
for (let i = currentStepIndex + 1; i < form.steps.length; i++) {
|
|||
|
|
const step = form.steps[i];
|
|||
|
|
if (!step.condition || step.condition(formData)) {
|
|||
|
|
return { step, index: i };
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return null;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const getSuggestions = (input) => {
|
|||
|
|
if (!input || input.length < 2) return [];
|
|||
|
|
|
|||
|
|
const inputLower = input.toLowerCase().trim();
|
|||
|
|
const matchingSuggestions = [];
|
|||
|
|
|
|||
|
|
// Search in keys
|
|||
|
|
Object.keys(suggestionDatabase).forEach(key => {
|
|||
|
|
if (key.toLowerCase().includes(inputLower)) {
|
|||
|
|
matchingSuggestions.push(...suggestionDatabase[key]);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Search in values
|
|||
|
|
Object.values(suggestionDatabase).forEach(suggestions => {
|
|||
|
|
suggestions.forEach(suggestion => {
|
|||
|
|
if (suggestion.toLowerCase().includes(inputLower) && !matchingSuggestions.includes(suggestion)) {
|
|||
|
|
matchingSuggestions.push(suggestion);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return matchingSuggestions.slice(0, 6);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// File handling functions
|
|||
|
|
const validateFile = (file) => {
|
|||
|
|
if (file.size > MAX_FILE_SIZE) {
|
|||
|
|
return `File ${file.name} is too large. Maximum size is 5MB.`;
|
|||
|
|
}
|
|||
|
|
if (!ALLOWED_FILE_TYPES.includes(file.type)) {
|
|||
|
|
return `File type ${file.type} is not allowed.`;
|
|||
|
|
}
|
|||
|
|
return null;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const getFileIcon = (fileType) => {
|
|||
|
|
if (fileType.startsWith("image/")) return "tabler-photo";
|
|||
|
|
if (fileType.includes("pdf")) return "tabler-file-type-pdf";
|
|||
|
|
if (fileType.includes("word")) return "tabler-file-type-doc";
|
|||
|
|
if (fileType.includes("text")) return "tabler-file-text";
|
|||
|
|
return "tabler-file";
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const getFileSize = (size) => {
|
|||
|
|
if (size < 1024) return `${size} B`;
|
|||
|
|
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`;
|
|||
|
|
return `${(size / (1024 * 1024)).toFixed(1)} MB`;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Message queue processing
|
|||
|
|
const processMessageQueue = async () => {
|
|||
|
|
if (isProcessingQueue.value || messageQueue.value.length === 0) return;
|
|||
|
|
|
|||
|
|
isProcessingQueue.value = true;
|
|||
|
|
|
|||
|
|
while (messageQueue.value.length > 0 && !isRequestCancelled.value) {
|
|||
|
|
const nextMessage = messageQueue.value.shift();
|
|||
|
|
|
|||
|
|
if (isRequestCancelled.value) break;
|
|||
|
|
|
|||
|
|
messages.value.push({ ...nextMessage, isAnimating: true });
|
|||
|
|
await nextTick();
|
|||
|
|
scrollToBottom();
|
|||
|
|
await new Promise(resolve => setTimeout(resolve, 300));
|
|||
|
|
|
|||
|
|
if (!isRequestCancelled.value) {
|
|||
|
|
const lastIndex = messages.value.length - 1;
|
|||
|
|
messages.value[lastIndex] = { ...messages.value[lastIndex], isAnimating: false };
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
isProcessingQueue.value = false;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Bot reply logic
|
|||
|
|
const getBotReply = (text) => {
|
|||
|
|
console.log("getBotReply called with:", text);
|
|||
|
|
|
|||
|
|
if (activeForm.value !== null) {
|
|||
|
|
return "Please respond to the current question first.";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const lower = text.toLowerCase();
|
|||
|
|
|
|||
|
|
if (lower === "/cancel") {
|
|||
|
|
activeForm.value = null;
|
|||
|
|
activeMultiForm.value = null;
|
|||
|
|
return "The form has been cancelled";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Resource Management
|
|||
|
|
if (text === "Resource Management" || lower === "resource management" || lower === "منابع" || lower === "مدیریت منابع") {
|
|||
|
|
setTimeout(() => startMultiForm("resourceManagement"), 100);
|
|||
|
|
return "Starting Resource Management form...";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// New Project
|
|||
|
|
if (text === "New Project" || text === "New Project ایجاد کنم" || lower === "new project" || lower === "نیو پروژکت" || lower === "پروژه جدید") {
|
|||
|
|
setTimeout(() => startMultiForm("newProject"), 100);
|
|||
|
|
return "Starting New Project form...";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Form triggers
|
|||
|
|
const formTriggers = {
|
|||
|
|
subscription: ["subscribe", "newsletter subscription"],
|
|||
|
|
feedback: ["feedback", "rate service"],
|
|||
|
|
demo: ["demo", "request demo"]
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
for (const [formId, triggers] of Object.entries(formTriggers)) {
|
|||
|
|
if (triggers.includes(text)) {
|
|||
|
|
return { type: "form", formId };
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Commands
|
|||
|
|
const commandResponses = {
|
|||
|
|
"/start": "Welcome! I'm ready to help you. 🎉",
|
|||
|
|
"/help": "Here's a list of things I can do... 🛠️",
|
|||
|
|
"/pricing": "Our pricing starts at $10/month. Would you like more details? 💸",
|
|||
|
|
"/faq": "Check out our FAQ page for common questions. ❓"
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return commandResponses[text] || null;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Form handling
|
|||
|
|
const handleFormResponse = (userResponse) => {
|
|||
|
|
if (!activeForm.value) return "Sorry, there was an error processing your response.";
|
|||
|
|
|
|||
|
|
const form = forms[activeForm.value];
|
|||
|
|
const normalizedResponse = form.options.find(option => option.toLowerCase() === userResponse.toLowerCase());
|
|||
|
|
|
|||
|
|
if (!normalizedResponse && !["yes", "no"].includes(userResponse.toLowerCase())) {
|
|||
|
|
return "Please respond with one of the provided options.";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
formResponses.value[form.id] = normalizedResponse || (userResponse.toLowerCase() === "yes" ? "Yes" : "No");
|
|||
|
|
const response = form.callback(formResponses.value[form.id]);
|
|||
|
|
activeForm.value = null;
|
|||
|
|
return response;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleMultiFormResponse = (userResponse) => {
|
|||
|
|
if (!activeMultiForm.value) {
|
|||
|
|
return { completed: true, message: "خطا در پردازش فرم." };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const form = forms[activeMultiForm.value];
|
|||
|
|
const currentStep = form.steps[currentFormStep.value];
|
|||
|
|
|
|||
|
|
multiFormData.value[currentStep.id] = userResponse;
|
|||
|
|
|
|||
|
|
if (pendingConfirmationFormId.value) {
|
|||
|
|
confirmationData.value = { ...confirmationData.value, ...multiFormData.value };
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
completed: true,
|
|||
|
|
data: confirmationData.value,
|
|||
|
|
showConfirmationAgain: true
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const nextValidStep = findNextValidStep(form, currentFormStep.value, multiFormData.value);
|
|||
|
|
|
|||
|
|
if (nextValidStep) {
|
|||
|
|
currentFormStep.value = nextValidStep.index;
|
|||
|
|
return {
|
|||
|
|
completed: false,
|
|||
|
|
nextStep: {
|
|||
|
|
...nextValidStep.step,
|
|||
|
|
stepNumber: nextValidStep.index + 1,
|
|||
|
|
totalSteps: form.steps.length,
|
|||
|
|
formTitle: form.title
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
} else {
|
|||
|
|
const callbackResult = form.callback ? form.callback(multiFormData.value) : null;
|
|||
|
|
return { completed: true, data: multiFormData.value, callbackResult };
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const startMultiForm = (formId) => {
|
|||
|
|
const form = forms[formId];
|
|||
|
|
if (!form) return;
|
|||
|
|
|
|||
|
|
activeMultiForm.value = formId;
|
|||
|
|
currentFormStep.value = 0;
|
|||
|
|
multiFormData.value = {};
|
|||
|
|
|
|||
|
|
|
|||
|
|
let firstValidStepIndex = 0;
|
|||
|
|
for (let i = 0; i < form.steps.length; i++) {
|
|||
|
|
const step = form.steps[i];
|
|||
|
|
if (!step.condition || step.condition(multiFormData.value)) {
|
|||
|
|
firstValidStepIndex = i;
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
currentFormStep.value = firstValidStepIndex;
|
|||
|
|
|
|||
|
|
messageQueue.value.push({
|
|||
|
|
sender: "bot",
|
|||
|
|
multiForm: {
|
|||
|
|
...form.steps[firstValidStepIndex],
|
|||
|
|
stepNumber: firstValidStepIndex + 1,
|
|||
|
|
totalSteps: form.steps.length,
|
|||
|
|
formTitle: form.title
|
|||
|
|
},
|
|||
|
|
type: "multi-form"
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Event handlers
|
|||
|
|
const sendMessage = async () => {
|
|||
|
|
if (isConfirmationActive.value) return;
|
|||
|
|
|
|||
|
|
const userMsg = msg.value.trim();
|
|||
|
|
if (!userMsg && selectedAttachments.value.length === 0) return;
|
|||
|
|
|
|||
|
|
if (projectOptions.value.includes(userMsg)) {
|
|||
|
|
handleProjectSelect(userMsg);
|
|||
|
|
msg.value = "";
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (userMsg === "/cancel") {
|
|||
|
|
activeForm.value = null;
|
|||
|
|
activeMultiForm.value = null;
|
|||
|
|
messageQueue.value.push({ text: "The form has been cancelled", sender: "bot" });
|
|||
|
|
msg.value = "";
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
isRequestCancelled.value = false;
|
|||
|
|
if (isInputCentered.value) isInputCentered.value = false;
|
|||
|
|
|
|||
|
|
// Handle file uploads
|
|||
|
|
if (selectedAttachments.value.length > 0) {
|
|||
|
|
const fileNames = selectedAttachments.value.map(f => f.name).join(", ");
|
|||
|
|
messageQueue.value.push({
|
|||
|
|
text: userMsg || `Files sent: ${fileNames}`,
|
|||
|
|
sender: "user",
|
|||
|
|
isAttachment: true,
|
|||
|
|
files: [...selectedAttachments.value]
|
|||
|
|
});
|
|||
|
|
selectedAttachments.value = [];
|
|||
|
|
} else {
|
|||
|
|
messageQueue.value.push({ text: userMsg, sender: "user" });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
msg.value = "";
|
|||
|
|
await nextTick();
|
|||
|
|
scrollToBottom();
|
|||
|
|
|
|||
|
|
if (!isProcessingQueue.value && messageQueue.value.length > 0) {
|
|||
|
|
processMessageQueue();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Handle different message types
|
|||
|
|
if (activeMultiForm.value) {
|
|||
|
|
const formResponse = handleMultiFormResponse(userMsg);
|
|||
|
|
if (formResponse.completed) {
|
|||
|
|
showDataConfirmation(multiFormData.value, activeMultiForm.value);
|
|||
|
|
activeMultiForm.value = null;
|
|||
|
|
currentFormStep.value = 0;
|
|||
|
|
} else if (formResponse.nextStep) {
|
|||
|
|
messageQueue.value.push({
|
|||
|
|
sender: "bot",
|
|||
|
|
multiForm: formResponse.nextStep,
|
|||
|
|
type: "multi-form"
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
} else if (activeForm.value) {
|
|||
|
|
const formResponse = handleFormResponse(userMsg);
|
|||
|
|
messageQueue.value.push({ text: formResponse, sender: "bot" });
|
|||
|
|
activeForm.value = null;
|
|||
|
|
} else {
|
|||
|
|
const botReply = getBotReply(userMsg);
|
|||
|
|
|
|||
|
|
if (botReply && typeof botReply === "object") {
|
|||
|
|
if (botReply.type === "multi-form") {
|
|||
|
|
isBotTyping.value = true;
|
|||
|
|
await new Promise(r => setTimeout(r, 500));
|
|||
|
|
activeMultiForm.value = botReply.formId;
|
|||
|
|
currentFormStep.value = 0;
|
|||
|
|
multiFormData.value = {};
|
|||
|
|
const form = forms[botReply.formId];
|
|||
|
|
messageQueue.value.push({
|
|||
|
|
sender: "bot",
|
|||
|
|
multiForm: {
|
|||
|
|
...form.steps[0],
|
|||
|
|
stepNumber: 1,
|
|||
|
|
totalSteps: form.steps.length,
|
|||
|
|
formTitle: form.title
|
|||
|
|
},
|
|||
|
|
type: "multi-form"
|
|||
|
|
});
|
|||
|
|
isBotTyping.value = false;
|
|||
|
|
} else if (botReply.type === "form") {
|
|||
|
|
activeForm.value = botReply.formId;
|
|||
|
|
const form = forms[botReply.formId];
|
|||
|
|
messageQueue.value.push({
|
|||
|
|
sender: "bot",
|
|||
|
|
text: form.question,
|
|||
|
|
form: {
|
|||
|
|
id: form.id,
|
|||
|
|
title: form.title,
|
|||
|
|
question: form.question,
|
|||
|
|
options: form.options
|
|||
|
|
},
|
|||
|
|
type: "form"
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
return;
|
|||
|
|
} else if (typeof botReply === "string") {
|
|||
|
|
messageQueue.value.push({ text: botReply, sender: "bot" });
|
|||
|
|
} else {
|
|||
|
|
// API call
|
|||
|
|
isBotTyping.value = true;
|
|||
|
|
abortController.value = new AbortController();
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const response = await fetch("/CallChatService.php", {
|
|||
|
|
method: "POST",
|
|||
|
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|||
|
|
body: new URLSearchParams({ message: userMsg }),
|
|||
|
|
signal: abortController.value.signal
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (response.ok) {
|
|||
|
|
const resultText = await response.text();
|
|||
|
|
const cleanText = cleanHtmlResponse(resultText);
|
|||
|
|
messageQueue.value.push({ text: cleanText, sender: "bot" });
|
|||
|
|
} else {
|
|||
|
|
messageQueue.value.push({ text: "Error receiving response from server", sender: "bot" });
|
|||
|
|
}
|
|||
|
|
} catch {
|
|||
|
|
if (!isRequestCancelled.value) {
|
|||
|
|
messageQueue.value.push({ text: "Could not connect to the server", sender: "bot" });
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
isBotTyping.value = false;
|
|||
|
|
await nextTick();
|
|||
|
|
scrollToBottom();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleProjectSelect = (option) => {
|
|||
|
|
selectedProject.value = option;
|
|||
|
|
showProjectMenu.value = false;
|
|||
|
|
|
|||
|
|
if (option === "+ New Project") {
|
|||
|
|
if (isInputCentered.value) isInputCentered.value = false;
|
|||
|
|
activeMultiForm.value = "newProject";
|
|||
|
|
currentFormStep.value = 0;
|
|||
|
|
multiFormData.value = {};
|
|||
|
|
const form = forms.newProject;
|
|||
|
|
messageQueue.value.push({
|
|||
|
|
sender: "bot",
|
|||
|
|
multiForm: {
|
|||
|
|
...form.steps[0],
|
|||
|
|
stepNumber: 1,
|
|||
|
|
totalSteps: form.steps.length,
|
|||
|
|
formTitle: form.title
|
|||
|
|
},
|
|||
|
|
type: "multi-form"
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleModelChange = (model) => {
|
|||
|
|
selectedModelIdentifier.value = model.identifier;
|
|||
|
|
showModelMenu.value = false;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const showDataConfirmation = (data, formId) => {
|
|||
|
|
confirmationData.value = { ...confirmationData.value, ...data };
|
|||
|
|
pendingConfirmationFormId.value = formId;
|
|||
|
|
|
|||
|
|
const form = forms[formId];
|
|||
|
|
const steps = form.steps;
|
|||
|
|
availableSteps.value = steps.map((step, index) => ({ ...step, stepIndex: index }));
|
|||
|
|
|
|||
|
|
messageQueue.value.push({
|
|||
|
|
sender: "bot",
|
|||
|
|
type: "confirmation",
|
|||
|
|
data: confirmationData.value,
|
|||
|
|
formTitle: form.title
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleConfirmation = (confirmed, message) => {
|
|||
|
|
if (confirmed) {
|
|||
|
|
showConfirmation.value = false;
|
|||
|
|
showStepSelection.value = false;
|
|||
|
|
isBotTyping.value = true;
|
|||
|
|
message.confirmed = true;
|
|||
|
|
|
|||
|
|
setTimeout(() => {
|
|||
|
|
if (pendingConfirmationFormId.value && forms[pendingConfirmationFormId.value]) {
|
|||
|
|
if (pendingConfirmationFormId.value === "newProject") {
|
|||
|
|
const name = confirmationData.value.projectName;
|
|||
|
|
if (!projectOptions.value.includes(name)) {
|
|||
|
|
const index = projectOptions.value.indexOf("+ New Project");
|
|||
|
|
projectOptions.value.splice(index, 0, name);
|
|||
|
|
const saved = projectOptions.value.filter(p => p !== "+ New Project");
|
|||
|
|
localStorage.setItem("projectOptions", JSON.stringify(saved));
|
|||
|
|
}
|
|||
|
|
selectedProject.value = name;
|
|||
|
|
console.log("✅ Project Data:", JSON.stringify(confirmationData.value, null, 2));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const form = forms[pendingConfirmationFormId.value];
|
|||
|
|
const finalMessage = form.callback(confirmationData.value);
|
|||
|
|
messageQueue.value.push({ text: finalMessage, sender: "bot" });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
isBotTyping.value = false;
|
|||
|
|
confirmationData.value = {};
|
|||
|
|
pendingConfirmationFormId.value = null;
|
|||
|
|
multiFormData.value = {};
|
|||
|
|
activeMultiForm.value = null;
|
|||
|
|
currentFormStep.value = 0;
|
|||
|
|
}, 800);
|
|||
|
|
} else {
|
|||
|
|
showConfirmation.value = false;
|
|||
|
|
showStepSelection.value = true;
|
|||
|
|
|
|||
|
|
messageQueue.value.push({
|
|||
|
|
sender: "bot",
|
|||
|
|
type: "step-selection",
|
|||
|
|
steps: availableSteps.value,
|
|||
|
|
formTitle: pendingConfirmationFormId.value && forms[pendingConfirmationFormId.value]
|
|||
|
|
? forms[pendingConfirmationFormId.value].title
|
|||
|
|
: "Unknown Form"
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleStepSelection = (stepIndex) => {
|
|||
|
|
const formId = pendingConfirmationFormId.value;
|
|||
|
|
if (!formId || !forms[formId]) return;
|
|||
|
|
|
|||
|
|
activeMultiForm.value = formId;
|
|||
|
|
currentFormStep.value = stepIndex;
|
|||
|
|
showStepSelection.value = false;
|
|||
|
|
|
|||
|
|
messageQueue.value.push({
|
|||
|
|
sender: "bot",
|
|||
|
|
multiForm: {
|
|||
|
|
...forms[formId].steps[stepIndex],
|
|||
|
|
stepNumber: stepIndex + 1,
|
|||
|
|
totalSteps: forms[formId].steps.length,
|
|||
|
|
formTitle: forms[formId].title,
|
|||
|
|
isEditing: true
|
|||
|
|
},
|
|||
|
|
type: "multi-form"
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleMultiFormOptionClick = (stepId, option) => {
|
|||
|
|
if (!activeMultiForm.value) return;
|
|||
|
|
|
|||
|
|
messageQueue.value.push({ text: option, sender: "user" });
|
|||
|
|
isBotTyping.value = true;
|
|||
|
|
isRequestCancelled.value = false;
|
|||
|
|
|
|||
|
|
setTimeout(() => {
|
|||
|
|
if (!isRequestCancelled.value) {
|
|||
|
|
isBotTyping.value = false;
|
|||
|
|
const formResponse = handleMultiFormResponse(option);
|
|||
|
|
|
|||
|
|
if (formResponse.completed) {
|
|||
|
|
const form = forms[activeMultiForm.value];
|
|||
|
|
|
|||
|
|
if (formResponse.showConfirmationAgain) {
|
|||
|
|
messageQueue.value.push({
|
|||
|
|
sender: "bot",
|
|||
|
|
type: "confirmation",
|
|||
|
|
data: confirmationData.value,
|
|||
|
|
formTitle: form.title
|
|||
|
|
});
|
|||
|
|
activeMultiForm.value = null;
|
|||
|
|
currentFormStep.value = 0;
|
|||
|
|
} else if (form.requiresConfirmation && !pendingConfirmationFormId.value) {
|
|||
|
|
showDataConfirmation(multiFormData.value, activeMultiForm.value);
|
|||
|
|
} else if (formResponse.callbackResult) {
|
|||
|
|
messageQueue.value.push({ text: formResponse.callbackResult, sender: "bot" });
|
|||
|
|
resetFormStates();
|
|||
|
|
} else {
|
|||
|
|
resetFormStates();
|
|||
|
|
}
|
|||
|
|
} else if (formResponse.nextStep) {
|
|||
|
|
messageQueue.value.push({
|
|||
|
|
sender: "bot",
|
|||
|
|
multiForm: formResponse.nextStep,
|
|||
|
|
type: "multi-form"
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}, 1000);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleFormOptionClick = (formId, option) => {
|
|||
|
|
if (!forms[formId] || answeredForms.value.has(formId)) return;
|
|||
|
|
|
|||
|
|
answeredForms.value.add(formId);
|
|||
|
|
formResponses.value[formId] = option;
|
|||
|
|
messageQueue.value.push({ text: option, sender: "user" });
|
|||
|
|
isBotTyping.value = true;
|
|||
|
|
|
|||
|
|
setTimeout(() => {
|
|||
|
|
isBotTyping.value = false;
|
|||
|
|
const response = forms[formId].callback(option);
|
|||
|
|
messageQueue.value.push({ text: response, sender: "bot" });
|
|||
|
|
activeForm.value = null;
|
|||
|
|
}, 1000);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Utility functions
|
|||
|
|
const resetFormStates = () => {
|
|||
|
|
activeMultiForm.value = null;
|
|||
|
|
currentFormStep.value = 0;
|
|||
|
|
multiFormData.value = {};
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const selectCommand = (command) => {
|
|||
|
|
msg.value = command;
|
|||
|
|
showCommandList.value = false;
|
|||
|
|
sendMessage();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const selectQuickReply = (text) => {
|
|||
|
|
if (activeMultiForm.value !== null) return;
|
|||
|
|
if (activeForm.value !== null) return;
|
|||
|
|
if (isConfirmationActive.value) return;
|
|||
|
|
|
|||
|
|
const clickedButton = event?.target?.closest('.quick-reply-transparent');
|
|||
|
|
if (clickedButton) {
|
|||
|
|
clickedButton.style.transform = 'scale(0.95)';
|
|||
|
|
setTimeout(() => {
|
|||
|
|
clickedButton.style.transform = '';
|
|||
|
|
}, 150);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
msg.value = text;
|
|||
|
|
sendMessage();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
|
|||
|
|
const selectSuggestion = (suggestion) => {
|
|||
|
|
msg.value = suggestion;
|
|||
|
|
showSuggestions.value = false;
|
|||
|
|
selectedSuggestionIndex.value = -1;
|
|||
|
|
nextTick(() => {
|
|||
|
|
refInputEl.value?.focus();
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleStop = () => {
|
|||
|
|
isRequestCancelled.value = true;
|
|||
|
|
isBotTyping.value = false;
|
|||
|
|
isProcessingQueue.value = false;
|
|||
|
|
|
|||
|
|
if (abortController.value) {
|
|||
|
|
abortController.value.abort();
|
|||
|
|
abortController.value = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
messageQueue.value = [];
|
|||
|
|
|
|||
|
|
messages.value.push({
|
|||
|
|
text: "Request stopped.",
|
|||
|
|
sender: "bot",
|
|||
|
|
isSystemMessage: true
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
nextTick(() => {
|
|||
|
|
scrollToBottom();
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const cancelForm = () => {
|
|||
|
|
const lastFormIndex = messages.value.findLastIndex(
|
|||
|
|
msg => msg.form || msg.multiForm || msg.type === "confirmation" || msg.type === "step-selection"
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
if (lastFormIndex !== -1) {
|
|||
|
|
messages.value[lastFormIndex] = {
|
|||
|
|
...messages.value[lastFormIndex],
|
|||
|
|
isDeleting: true
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
setTimeout(() => {
|
|||
|
|
messages.value.splice(lastFormIndex, 1);
|
|||
|
|
resetAllFormStates();
|
|||
|
|
messageQueue.value.push({
|
|||
|
|
text: "Form canceled",
|
|||
|
|
sender: "bot",
|
|||
|
|
isSystemMessage: true
|
|||
|
|
});
|
|||
|
|
}, 300);
|
|||
|
|
} else {
|
|||
|
|
resetAllFormStates();
|
|||
|
|
messageQueue.value.push({
|
|||
|
|
text: "Form canceled",
|
|||
|
|
sender: "bot",
|
|||
|
|
isSystemMessage: true
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const resetAllFormStates = () => {
|
|||
|
|
activeForm.value = null;
|
|||
|
|
activeMultiForm.value = null;
|
|||
|
|
confirmationData.value = {};
|
|||
|
|
pendingConfirmationFormId.value = null;
|
|||
|
|
currentFormStep.value = 0;
|
|||
|
|
multiFormData.value = {};
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// File handling
|
|||
|
|
const openUploadModal = () => {
|
|||
|
|
isUploadModalOpen.value = true;
|
|||
|
|
fileError.value = "";
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const closeUploadModal = () => {
|
|||
|
|
isUploadModalOpen.value = false;
|
|||
|
|
fileError.value = "";
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const triggerFileInput = () => {
|
|||
|
|
fileInputRef.value.click();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleFileUpload = (event) => {
|
|||
|
|
fileError.value = "";
|
|||
|
|
const files = event.target.files;
|
|||
|
|
|
|||
|
|
if (!files || !files.length) return;
|
|||
|
|
|
|||
|
|
if (selectedAttachments.value.length + files.length > MAX_FILES) {
|
|||
|
|
fileError.value = `You can upload maximum ${MAX_FILES} files at once.`;
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for (const file of files) {
|
|||
|
|
const error = validateFile(file);
|
|||
|
|
if (error) {
|
|||
|
|
fileError.value = error;
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
selectedAttachments.value.push(file);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
event.target.value = "";
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const removeAttachment = (index) => {
|
|||
|
|
selectedAttachments.value = selectedAttachments.value.filter((_, i) => i !== index);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Input handling
|
|||
|
|
const handleCompositionStart = () => {
|
|||
|
|
isComposing.value = true;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleCompositionEnd = () => {
|
|||
|
|
isComposing.value = false;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleKeyDown = (e) => {
|
|||
|
|
if (showSuggestions.value && suggestions.value.length > 0) {
|
|||
|
|
if (e.key === "ArrowDown") {
|
|||
|
|
e.preventDefault();
|
|||
|
|
selectedSuggestionIndex.value = Math.min(
|
|||
|
|
selectedSuggestionIndex.value + 1,
|
|||
|
|
suggestions.value.length - 1
|
|||
|
|
);
|
|||
|
|
return;
|
|||
|
|
} else if (e.key === "ArrowUp") {
|
|||
|
|
e.preventDefault();
|
|||
|
|
selectedSuggestionIndex.value = Math.max(selectedSuggestionIndex.value - 1, -1);
|
|||
|
|
return;
|
|||
|
|
} else if (e.key === "Enter" && selectedSuggestionIndex.value >= 0) {
|
|||
|
|
e.preventDefault();
|
|||
|
|
selectSuggestion(suggestions.value[selectedSuggestionIndex.value]);
|
|||
|
|
return;
|
|||
|
|
} else if (e.key === "Escape") {
|
|||
|
|
showSuggestions.value = false;
|
|||
|
|
selectedSuggestionIndex.value = -1;
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (e.key === "Enter" && !e.shiftKey && !isComposing.value) {
|
|||
|
|
e.preventDefault();
|
|||
|
|
sendMessage();
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Chat management
|
|||
|
|
const resetChat = () => {
|
|||
|
|
if (isBotTyping.value || isProcessingQueue.value) {
|
|||
|
|
isRequestCancelled.value = true;
|
|||
|
|
isBotTyping.value = false;
|
|||
|
|
if (abortController.value) {
|
|||
|
|
abortController.value.abort();
|
|||
|
|
abortController.value = null;
|
|||
|
|
}
|
|||
|
|
messageQueue.value = [];
|
|||
|
|
isProcessingQueue.value = false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const isChatEmpty = messages.value.length === 0 && !activeForm.value &&
|
|||
|
|
!activeMultiForm.value && selectedAttachments.value.length === 0 && !msg.value.trim();
|
|||
|
|
|
|||
|
|
if (isChatEmpty) {
|
|||
|
|
selectedProject.value = "";
|
|||
|
|
selectedModelIdentifier.value = MODELS.value[0].identifier;
|
|||
|
|
isInputCentered.value = true;
|
|||
|
|
msg.value = "";
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Backup current state
|
|||
|
|
chatBackup.value = {
|
|||
|
|
messages: [...messages.value],
|
|||
|
|
selectedProject: selectedProject.value,
|
|||
|
|
selectedModelIdentifier: selectedModelIdentifier.value,
|
|||
|
|
isInputCentered: isInputCentered.value,
|
|||
|
|
msg: msg.value,
|
|||
|
|
activeForm: activeForm.value,
|
|||
|
|
activeMultiForm: activeMultiForm.value,
|
|||
|
|
formResponses: { ...formResponses.value },
|
|||
|
|
multiFormData: { ...multiFormData.value },
|
|||
|
|
confirmationData: { ...confirmationData.value },
|
|||
|
|
currentFormStep: currentFormStep.value,
|
|||
|
|
answeredForms: new Set(answeredForms.value),
|
|||
|
|
pendingConfirmationFormId: pendingConfirmationFormId.value,
|
|||
|
|
messageQueue: [...messageQueue.value],
|
|||
|
|
selectedAttachments: [...selectedAttachments.value],
|
|||
|
|
suggestions: [...suggestions.value],
|
|||
|
|
selectedSuggestionIndex: selectedSuggestionIndex.value,
|
|||
|
|
isBotTyping: isBotTyping.value,
|
|||
|
|
isProcessingQueue: isProcessingQueue.value
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Reset all states
|
|||
|
|
resetAllStates();
|
|||
|
|
|
|||
|
|
// Show undo popup
|
|||
|
|
showUndoPopup.value = true;
|
|||
|
|
undoProgress.value = 100;
|
|||
|
|
if (undoInterval.value) clearInterval(undoInterval.value);
|
|||
|
|
undoInterval.value = setInterval(() => {
|
|||
|
|
undoProgress.value = Math.max(0, undoProgress.value - 2);
|
|||
|
|
}, 100);
|
|||
|
|
if (undoTimer.value) clearTimeout(undoTimer.value);
|
|||
|
|
undoTimer.value = setTimeout(() => {
|
|||
|
|
showUndoPopup.value = false;
|
|||
|
|
chatBackup.value = null;
|
|||
|
|
clearInterval(undoInterval.value);
|
|||
|
|
undoInterval.value = null;
|
|||
|
|
}, 5000);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const resetAllStates = () => {
|
|||
|
|
messages.value = [];
|
|||
|
|
selectedProject.value = "";
|
|||
|
|
selectedModelIdentifier.value = MODELS.value[0].identifier;
|
|||
|
|
isInputCentered.value = true;
|
|||
|
|
msg.value = "";
|
|||
|
|
activeForm.value = null;
|
|||
|
|
activeMultiForm.value = null;
|
|||
|
|
formResponses.value = {};
|
|||
|
|
multiFormData.value = {};
|
|||
|
|
confirmationData.value = {};
|
|||
|
|
currentFormStep.value = 0;
|
|||
|
|
answeredForms.value = new Set();
|
|||
|
|
pendingConfirmationFormId.value = null;
|
|||
|
|
messageQueue.value = [];
|
|||
|
|
isProcessingQueue.value = false;
|
|||
|
|
isBotTyping.value = false;
|
|||
|
|
selectedAttachments.value = [];
|
|||
|
|
isUploadModalOpen.value = false;
|
|||
|
|
showConfirmation.value = false;
|
|||
|
|
showStepSelection.value = false;
|
|||
|
|
fileError.value = "";
|
|||
|
|
showModelMenu.value = false;
|
|||
|
|
showProjectMenu.value = false;
|
|||
|
|
showCommandList.value = false;
|
|||
|
|
showSuggestions.value = false;
|
|||
|
|
suggestions.value = [];
|
|||
|
|
selectedSuggestionIndex.value = -1;
|
|||
|
|
filteredCommands.value = [];
|
|||
|
|
isComposing.value = false;
|
|||
|
|
availableSteps.value = [];
|
|||
|
|
isRequestCancelled.value = false;
|
|||
|
|
abortController.value = null;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const undoReset = () => {
|
|||
|
|
if (!chatBackup.value) return;
|
|||
|
|
|
|||
|
|
// Restore all states from backup
|
|||
|
|
Object.keys(chatBackup.value).forEach(key => {
|
|||
|
|
if (key === 'answeredForms') {
|
|||
|
|
answeredForms.value = chatBackup.value[key];
|
|||
|
|
} else {
|
|||
|
|
eval(`${key}.value = chatBackup.value.${key}`);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
showUndoPopup.value = false;
|
|||
|
|
if (undoTimer.value) {
|
|||
|
|
clearTimeout(undoTimer.value);
|
|||
|
|
undoTimer.value = null;
|
|||
|
|
}
|
|||
|
|
if (undoInterval.value) {
|
|||
|
|
clearInterval(undoInterval.value);
|
|||
|
|
undoInterval.value = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
chatBackup.value = null;
|
|||
|
|
nextTick(() => {
|
|||
|
|
scrollToBottom();
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const closeUndoPopup = () => {
|
|||
|
|
showUndoPopup.value = false;
|
|||
|
|
if (undoTimer.value) {
|
|||
|
|
clearTimeout(undoTimer.value);
|
|||
|
|
undoTimer.value = null;
|
|||
|
|
}
|
|||
|
|
if (undoInterval.value) {
|
|||
|
|
clearInterval(undoInterval.value);
|
|||
|
|
undoInterval.value = null;
|
|||
|
|
}
|
|||
|
|
chatBackup.value = null;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Watchers
|
|||
|
|
watch(messageQueue, (newQueue) => {
|
|||
|
|
if (newQueue.length > 0 && !isProcessingQueue.value) {
|
|||
|
|
processMessageQueue();
|
|||
|
|
}
|
|||
|
|
}, { deep: true });
|
|||
|
|
|
|||
|
|
watch(msg, (val) => {
|
|||
|
|
if (val.startsWith("/")) {
|
|||
|
|
filteredCommands.value = COMMANDS.filter(c => c.startsWith(val));
|
|||
|
|
showCommandList.value = filteredCommands.value.length > 0;
|
|||
|
|
showSuggestions.value = false;
|
|||
|
|
} else {
|
|||
|
|
showCommandList.value = false;
|
|||
|
|
|
|||
|
|
if (val && val.length >= 2) {
|
|||
|
|
suggestions.value = getSuggestions(val);
|
|||
|
|
showSuggestions.value = suggestions.value.length > 0;
|
|||
|
|
selectedSuggestionIndex.value = -1;
|
|||
|
|
} else {
|
|||
|
|
showSuggestions.value = false;
|
|||
|
|
suggestions.value = [];
|
|||
|
|
selectedSuggestionIndex.value = -1;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
watch([activeForm, activeMultiForm, isConfirmationActive], ([form, multiForm, confirmation]) => {
|
|||
|
|
if (form !== null || multiForm !== null || confirmation) {
|
|||
|
|
console.log('Quick replies hidden due to active form');
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Lifecycle
|
|||
|
|
onMounted(() => {
|
|||
|
|
processMessageQueue();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Expose necessary functions and refs for template usage
|
|||
|
|
const quickReplies = QUICK_REPLIES;
|
|||
|
|
const models = MODELS;
|
|||
|
|
const commands = COMMANDS;
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<template>
|
|||
|
|
<VLayout class="chat-app-layout" style="z-index: 0">
|
|||
|
|
<VMain class="chat-content-container">
|
|||
|
|
<PerfectScrollbar ref="scrollEl" tag="ul" :options="{ wheelPropagation: false }"
|
|||
|
|
class="chat-log-scrollable px-4 py-3">
|
|||
|
|
<TransitionGroup name="message">
|
|||
|
|
<li v-for="(message, index) in messages" :key="index" class="my-3 d-flex" :class="[
|
|||
|
|
message.sender === 'user' ? 'justify-end' : 'justify-start',
|
|||
|
|
message.isAnimating ? 'is-animating' : '',
|
|||
|
|
message.sender === 'user' ? 'user-message' : 'bot-message',
|
|||
|
|
message.isSystemMessage ? 'system-message' : '',
|
|||
|
|
]">
|
|||
|
|
<template v-if="message.sender !== 'user'">
|
|||
|
|
<VAvatar size="32" :color="message.isSystemMessage ? 'grey' : activeModelColor" variant="tonal"
|
|||
|
|
class="me-2 message-avatar">
|
|||
|
|
<VIcon :icon="message.isSystemMessage
|
|||
|
|
? 'tabler-info-circle'
|
|||
|
|
: 'tabler-robot'
|
|||
|
|
" />
|
|||
|
|
</VAvatar>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<div class="pa-3 rounded-lg message-bubble" :class="[
|
|||
|
|
message.sender === 'user'
|
|||
|
|
? 'bg-primary text-white'
|
|||
|
|
: message.isSystemMessage
|
|||
|
|
? 'bg-grey-lighten-3 text-grey-darken-3'
|
|||
|
|
: `bg-${activeModelBgColor} text-white`,
|
|||
|
|
message.isAnimating ? 'animate-message' : '',
|
|||
|
|
]">
|
|||
|
|
<div v-if="!message.form && !message.multiForm && !message.type">
|
|||
|
|
{{ message.text }}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div v-else-if="message.form" class="form-container">
|
|||
|
|
<div class="form-title font-weight-bold mb-2">
|
|||
|
|
{{ message.form.title }}
|
|||
|
|
</div>
|
|||
|
|
<div class="form-question mb-3">
|
|||
|
|
{{ message.form.question }}
|
|||
|
|
</div>
|
|||
|
|
<div class="form-options d-flex gap-2">
|
|||
|
|
<VBtn v-for="(option, optIndex) in message.form.options" :key="optIndex" :color="activeModelColor"
|
|||
|
|
variant="outlined" size="small" class="form-option-btn"
|
|||
|
|
@click="handleFormOptionClick(message.form.id, option)">
|
|||
|
|
{{ option }}
|
|||
|
|
</VBtn>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div v-else-if="message.multiForm" class="multi-form-container">
|
|||
|
|
<div class="multi-form-header mb-3">
|
|||
|
|
<div class="d-flex justify-space-between align-center mb-2">
|
|||
|
|
<div class="form-title font-weight-bold">
|
|||
|
|
{{ message.multiForm.formTitle }}
|
|||
|
|
</div>
|
|||
|
|
<div class="d-flex align-center">
|
|||
|
|
<VChip :color="activeModelColor" size="small" variant="tonal">
|
|||
|
|
{{ message.multiForm.stepNumber }}/{{
|
|||
|
|
message.multiForm.totalSteps
|
|||
|
|
}}
|
|||
|
|
</VChip>
|
|||
|
|
<VBtn icon size="x-small" @click="cancelForm" class="ms-2">
|
|||
|
|
<VIcon icon="tabler-x" />
|
|||
|
|
</VBtn>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<VProgressLinear :model-value="(message.multiForm.stepNumber /
|
|||
|
|
message.multiForm.totalSteps) *
|
|||
|
|
100
|
|||
|
|
" :color="activeModelColor" height="4" rounded class="mb-3"></VProgressLinear>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="step-title font-weight-medium mb-2">
|
|||
|
|
{{ message.multiForm.title }}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="form-question mb-3">
|
|||
|
|
{{ message.multiForm.question }}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div v-if="message.multiForm.type === 'options'" class="form-options">
|
|||
|
|
<div class="d-flex flex-wrap gap-2">
|
|||
|
|
<VBtn v-for="(option, optIndex) in message.multiForm.options" :key="optIndex"
|
|||
|
|
:color="activeModelColor" variant="outlined" size="small" class="form-option-btn" @click="
|
|||
|
|
handleMultiFormOptionClick(message.multiForm.id, option)
|
|||
|
|
">
|
|||
|
|
{{ option }}
|
|||
|
|
</VBtn>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div v-else-if="message.type === 'confirmation'" class="confirmation-container">
|
|||
|
|
<div class="form-title font-weight-bold mb-3">
|
|||
|
|
{{ message.formTitle }} - Information verification
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="confirmation-data mb-4">
|
|||
|
|
<div v-for="(value, key) in message.data" :key="key" class="data-item mb-2">
|
|||
|
|
<span class="font-weight-medium">{{ key }}:</span>
|
|||
|
|
<span class="ms-2">{{
|
|||
|
|
Array.isArray(value) ? value.join(", ") : value
|
|||
|
|
}}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div v-if="!message.confirmed" class="confirmation-actions d-flex gap-2">
|
|||
|
|
<VBtn :color="activeModelColor" variant="flat" size="small"
|
|||
|
|
@click="handleConfirmation(true, message)">
|
|||
|
|
Confirm and send
|
|||
|
|
</VBtn>
|
|||
|
|
<VBtn :color="activeModelColor" variant="outlined" size="small"
|
|||
|
|
@click="handleConfirmation(false, message)">
|
|||
|
|
Edit information
|
|||
|
|
</VBtn>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div v-else-if="message.type === 'step-selection'" class="step-selection-container">
|
|||
|
|
<div class="form-title font-weight-bold mb-3">
|
|||
|
|
{{ message.formTitle }} - Select step to edit
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="text-body-2 mb-3">
|
|||
|
|
Which stage do you want to return to?
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="steps-list d-flex flex-column gap-2">
|
|||
|
|
<VBtn v-for="(step, stepIndex) in message.steps" :key="stepIndex" :color="activeModelColor"
|
|||
|
|
variant="outlined" size="small" class="text-start justify-start"
|
|||
|
|
@click="handleStepSelection(step.stepIndex)">
|
|||
|
|
{{ step.title }}
|
|||
|
|
</VBtn>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div v-if="message.isAttachment && message.files" class="attachment-preview mt-2">
|
|||
|
|
<div v-for="(file, fileIndex) in message.files" :key="fileIndex" class="d-flex align-center mb-1">
|
|||
|
|
<VIcon :icon="getFileIcon(file.type)" size="small" class="me-1"></VIcon>
|
|||
|
|
<span class="text-caption">{{ file.name }} ({{ getFileSize(file.size) }})</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<template v-if="message.sender === 'user'">
|
|||
|
|
<VAvatar size="32" color="grey" variant="tonal" class="ms-2 message-avatar">
|
|||
|
|
<VIcon icon="tabler-user" />
|
|||
|
|
</VAvatar>
|
|||
|
|
</template>
|
|||
|
|
</li>
|
|||
|
|
</TransitionGroup>
|
|||
|
|
|
|||
|
|
<li v-if="isBotTyping" class="my-3 d-flex justify-start typing-indicator-container">
|
|||
|
|
<VAvatar size="32" :color="activeModelColor" variant="tonal" class="me-2 message-avatar">
|
|||
|
|
<VIcon icon="tabler-robot" />
|
|||
|
|
</VAvatar>
|
|||
|
|
|
|||
|
|
<div class="pa-3 rounded-lg message-bubble" :class="`bg-${activeModelBgColor} text-white`"
|
|||
|
|
:style="`--typing-shadow-color: rgba(var(--v-theme-${activeModelColor}), 0.3)`">
|
|||
|
|
<div class="bot-typing-indicator">
|
|||
|
|
<span class="dot"></span>
|
|||
|
|
<span class="dot"></span>
|
|||
|
|
<span class="dot"></span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</li>
|
|||
|
|
</PerfectScrollbar>
|
|||
|
|
|
|||
|
|
<div v-if="isInputCentered" class="welcome-message text-center pa-4">
|
|||
|
|
<h2 class="text-h3 mb-2">Welcome to <b>MegaTegra</b> Chatbot!</h2>
|
|||
|
|
<p class="text-body-1">Type a message below to get started.</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="input-wrapper" :class="{
|
|||
|
|
'centered-input': isInputCentered,
|
|||
|
|
'bottom-input': !isInputCentered,
|
|||
|
|
}">
|
|||
|
|
<div class="chat-input-container rounded-3xl pa-3 mb-2" :class="{ 'pt-0': selectedAttachments.length === 0 }"
|
|||
|
|
:style="`border: 1px solid rgba(var(--v-theme-${activeModelColor}), 0.5);
|
|||
|
|
--active-model-color: var(--v-theme-${activeModelColor})`">
|
|||
|
|
<div v-if="selectedAttachments.length > 0" class="d-flex flex-wrap gap-2 mt-3 mb-2 attachment-chip-container">
|
|||
|
|
<VChip v-for="(file, index) in selectedAttachments" :key="index" closable
|
|||
|
|
@click:close="removeAttachment(index)" color="grey-lighten-3" class="align-center attachment-chip">
|
|||
|
|
<VIcon start :icon="getFileIcon(file.type)" size="small"></VIcon>
|
|||
|
|
<span class="attachment-name">{{ file.name }}</span>
|
|||
|
|
<span class="attachment-size">({{ getFileSize(file.size) }})</span>
|
|||
|
|
</VChip>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="position-relative">
|
|||
|
|
<VTextarea v-model="msg" auto-grow rows="1" row-height="30"
|
|||
|
|
:disabled="isInputDisabled || isConfirmationActive" hide-details variant="plain" persistent-placeholder
|
|||
|
|
density="comfortable" class="chat-textarea" placeholder="Type your message..." no-resize
|
|||
|
|
@keydown="handleKeyDown" @compositionstart="handleCompositionStart"
|
|||
|
|
@compositionend="handleCompositionEnd"></VTextarea>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div v-if="showSuggestions" class="suggestions-dropdown rounded pa-2">
|
|||
|
|
<div class="suggestions-header text-caption text-grey-darken-1 mb-1 px-2">
|
|||
|
|
Suggestions
|
|||
|
|
</div>
|
|||
|
|
<div v-for="(suggestion, index) in suggestions" :key="index"
|
|||
|
|
class="suggestion-item pa-2 rounded d-flex align-center" :class="{
|
|||
|
|
'suggestion-selected': selectedSuggestionIndex === index,
|
|||
|
|
}" @click="selectSuggestion(suggestion)" @mouseenter="selectedSuggestionIndex = index">
|
|||
|
|
<VIcon icon="tabler-search" size="16" class="me-2 text-grey-darken-1"></VIcon>
|
|||
|
|
<span class="suggestion-text">{{ suggestion }}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div v-if="showCommandList" class="command-dropdown rounded pa-2">
|
|||
|
|
<div v-for="(cmd, i) in filteredCommands" :key="i" class="command-item pa-2 rounded"
|
|||
|
|
@click="selectCommand(cmd)">
|
|||
|
|
{{ cmd }}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="d-flex align-center justify-space-between mt-1 input-toolbar">
|
|||
|
|
<div class="d-flex gap-2 align-center toolbar-buttons">
|
|||
|
|
<VBtn icon variant="text" density="comfortable"
|
|||
|
|
color="rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity))" @click="resetChat">
|
|||
|
|
<VIcon icon="tabler-message-circle-plus" />
|
|||
|
|
</VBtn>
|
|||
|
|
|
|||
|
|
<VBtn icon variant="text" density="comfortable" :color="activeModelColor" @click="triggerFileInput">
|
|||
|
|
<VIcon icon="tabler-paperclip"></VIcon>
|
|||
|
|
<input ref="fileInputRef" type="file" multiple class="d-none" @change="handleFileUpload" />
|
|||
|
|
</VBtn>
|
|||
|
|
|
|||
|
|
<VBtn :color="isWebSearchEnabled ? 'primary' : 'grey-darken-1'"
|
|||
|
|
:variant="isWebSearchEnabled ? 'tonal' : 'text'" :elevation="isWebSearchEnabled ? 8 : 2"
|
|||
|
|
density="comfortable" class="text-none web-search-btn"
|
|||
|
|
:class="{ 'web-search-active': isWebSearchEnabled }" @click="toggleWebSearch">
|
|||
|
|
<VIcon icon="tabler-analyze" class="me-1 web-icon" :class="{ 'web-icon-active': isWebSearchEnabled }" />
|
|||
|
|
<span class="d-none d-sm-block web-text">Scenario</span>
|
|||
|
|
</VBtn>
|
|||
|
|
|
|||
|
|
<VMenu v-model="showProjectMenu" location="top">
|
|||
|
|
<template #activator="{ props }">
|
|||
|
|
<VBtn v-bind="props" :color="selectedProject === '+ New Project'
|
|||
|
|
? 'primary'
|
|||
|
|
: selectedProject
|
|||
|
|
? 'white'
|
|||
|
|
: 'secondary'
|
|||
|
|
" variant="text" density="comfortable" class="text-none">
|
|||
|
|
<span class="d-none d-sm-block">
|
|||
|
|
{{ selectedProject || "Project Menu" }}
|
|||
|
|
</span>
|
|||
|
|
<span class="d-block d-sm-none"> Project </span>
|
|||
|
|
|
|||
|
|
<VIcon icon="tabler-chevron-down" class="ms-1" />
|
|||
|
|
</VBtn>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<VList density="compact" max-height="200">
|
|||
|
|
<VListItem v-for="option in projectOptions" :key="option" @click="handleProjectSelect(option)" :class="{
|
|||
|
|
'bg-primary text-white':
|
|||
|
|
option === selectedProject &&
|
|||
|
|
option !== '+ New Project',
|
|||
|
|
}">
|
|||
|
|
<template #prepend v-if="option === selectedProject && option !== '+ New Project'
|
|||
|
|
">
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<VListItemTitle :class="option === '+ New Project' ? 'text-primary' : ''">
|
|||
|
|
{{ option }}
|
|||
|
|
</VListItemTitle>
|
|||
|
|
</VListItem>
|
|||
|
|
</VList>
|
|||
|
|
</VMenu>
|
|||
|
|
|
|||
|
|
<!-- <VMenu v-model="showModelMenu" location="top">
|
|||
|
|
<template #activator="{ props }">
|
|||
|
|
<VBtn v-bind="props" :color="activeModelColor" variant="text" class="text-none model-select-btn"
|
|||
|
|
density="comfortable">
|
|||
|
|
<span class="d-none d-sm-block">{{
|
|||
|
|
selectedModelName
|
|||
|
|
}}</span>
|
|||
|
|
<span class="d-block d-sm-none">Model</span>
|
|||
|
|
<VIcon icon="tabler-chevron-down" class="ms-1"></VIcon>
|
|||
|
|
</VBtn>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<VList density="compact" max-height="300">
|
|||
|
|
<VListItem v-for="model in models" :key="model.identifier" @click="handleModelChange(model)" :class="{
|
|||
|
|
'bg-primary text-white':
|
|||
|
|
selectedModelIdentifier === model.identifier,
|
|||
|
|
}">
|
|||
|
|
<template #prepend>
|
|||
|
|
<div class="model-color-indicator me-2"
|
|||
|
|
:style="`background-color: var(--v-theme-${model.color}); width: 12px; height: 12px; border-radius: 50%`">
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<template #append v-if="selectedModelIdentifier === model.identifier">
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<VListItemTitle>{{ model.name }}</VListItemTitle>
|
|||
|
|
</VListItem>
|
|||
|
|
</VList>
|
|||
|
|
</VMenu> -->
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<VBtn v-if="!isBotTyping" icon :color="activeModelColor" @click="sendMessage" :disabled="isInputDisabled ||
|
|||
|
|
(!msg.trim() && selectedAttachments.length === 0)
|
|||
|
|
">
|
|||
|
|
<VIcon icon="tabler-send"></VIcon>
|
|||
|
|
</VBtn>
|
|||
|
|
|
|||
|
|
<VBtn v-else icon :color="activeModelColor" @click="handleStop">
|
|||
|
|
<VIcon icon="tabler-square"></VIcon>
|
|||
|
|
</VBtn>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<Transition name="quick-replies-fade" mode="out-in" appear>
|
|||
|
|
<div v-if="isQuickRepliesVisible && !activeForm && !activeMultiForm && !isConfirmationActive"
|
|||
|
|
class="quick-replies-container" key="quick-replies">
|
|||
|
|
<TransitionGroup name="quick-reply" appear>
|
|||
|
|
<div v-for="(reply, index) in quickReplies" :key="reply" class="quick-reply-item"
|
|||
|
|
:style="{ transitionDelay: `${index * 50}ms` }">
|
|||
|
|
<VBtn class="quick-reply-transparent" variant="text" size="small" :style="`border: 1px solid rgba(var(--v-theme-${activeModelColor}), 0.5) !important;
|
|||
|
|
--hover-bg-color: rgba(var(--v-theme-${activeModelColor}), 0.1);
|
|||
|
|
--hover-border-color: rgba(var(--v-theme-${activeModelColor}), 0.8)`"
|
|||
|
|
@click="selectQuickReply(reply)">
|
|||
|
|
{{ reply }}
|
|||
|
|
</VBtn>
|
|||
|
|
</div>
|
|||
|
|
</TransitionGroup>
|
|||
|
|
</div>
|
|||
|
|
</Transition>
|
|||
|
|
</div>
|
|||
|
|
</VMain>
|
|||
|
|
|
|||
|
|
<VDialog v-model="isUploadModalOpen" max-width="500">
|
|||
|
|
<VCard>
|
|||
|
|
<VCardTitle class="pb-2 pt-4">
|
|||
|
|
<span class="text-h5">Upload Files</span>
|
|||
|
|
</VCardTitle>
|
|||
|
|
|
|||
|
|
<VCardText>
|
|||
|
|
<div class="text-subtitle-2 mb-4">
|
|||
|
|
Select files to upload (Max 5 files, 5MB each)
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="upload-area pa-6 border-dashed rounded d-flex flex-column align-center justify-center"
|
|||
|
|
@click="triggerFileInput" @dragover.prevent @drop.prevent="
|
|||
|
|
(e) =>
|
|||
|
|
handleFileUpload({ target: { files: e.dataTransfer.files } })
|
|||
|
|
">
|
|||
|
|
<VIcon :icon="'tabler-upload'" :size="36" :color="activeModelColor" class="mb-3"></VIcon>
|
|||
|
|
<div class="text-body-1 mb-1">
|
|||
|
|
Drag files here or click to upload
|
|||
|
|
</div>
|
|||
|
|
<div class="text-caption text-grey">
|
|||
|
|
Supported formats: Images, PDF, DOC, DOCX, TXT
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<input ref="fileInputRef" type="file" multiple class="d-none" @change="handleFileUpload" />
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div v-if="fileError" class="text-error mt-2">
|
|||
|
|
{{ fileError }}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div v-if="selectedAttachments.length > 0">
|
|||
|
|
<div class="text-subtitle-2 mt-4 mb-2">Selected files:</div>
|
|||
|
|
<VList density="compact" class="bg-grey-lighten-5 rounded">
|
|||
|
|
<VListItem v-for="(file, index) in selectedAttachments" :key="index">
|
|||
|
|
<template v-slot:prepend>
|
|||
|
|
<VIcon :icon="getFileIcon(file.type)"></VIcon>
|
|||
|
|
</template>
|
|||
|
|
<VListItemTitle>{{ file.name }}</VListItemTitle>
|
|||
|
|
<VListItemSubtitle>{{
|
|||
|
|
getFileSize(file.size)
|
|||
|
|
}}</VListItemSubtitle>
|
|||
|
|
<template v-slot:append>
|
|||
|
|
<VBtn icon size="small" variant="text" @click="removeAttachment(index)">
|
|||
|
|
<VIcon icon="tabler-x"></VIcon>
|
|||
|
|
</VBtn>
|
|||
|
|
</template>
|
|||
|
|
</VListItem>
|
|||
|
|
</VList>
|
|||
|
|
</div>
|
|||
|
|
</VCardText>
|
|||
|
|
|
|||
|
|
<VCardActions class="pb-4 px-4">
|
|||
|
|
<VSpacer></VSpacer>
|
|||
|
|
<VBtn color="grey-darken-1" variant="text" @click="closeUploadModal">
|
|||
|
|
Cancel
|
|||
|
|
</VBtn>
|
|||
|
|
<VBtn :color="activeModelColor" variant="tonal" @click="closeUploadModal"
|
|||
|
|
:disabled="selectedAttachments.length === 0">
|
|||
|
|
Done
|
|||
|
|
</VBtn>
|
|||
|
|
</VCardActions>
|
|||
|
|
</VCard>
|
|||
|
|
</VDialog>
|
|||
|
|
|
|||
|
|
<VSnackbar v-model="showUndoPopup" location="top right" :timeout="-1" color="primary" elevation="8" rounded="lg"
|
|||
|
|
class="undo-popup">
|
|||
|
|
<div class="d-flex align-center justify-space-between" style="width: 100%">
|
|||
|
|
<div class="d-flex align-center">
|
|||
|
|
<span class="text-body-2">Click Undo for return</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="d-flex align-center ms-3">
|
|||
|
|
<VBtn variant="text" color="white" size="small" @click="undoReset" class="me-2">
|
|||
|
|
<VIcon size="16">tabler-arrow-back-up</VIcon>
|
|||
|
|
<span class="ms-1">Undo</span>
|
|||
|
|
</VBtn>
|
|||
|
|
|
|||
|
|
<VBtn variant="text" color="white" size="small" @click="closeUndoPopup" icon>
|
|||
|
|
<VIcon size="16">tabler-x</VIcon>
|
|||
|
|
</VBtn>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<VProgressLinear :model-value="undoProgress" height="4" color="white" rounded class="mt-2" />
|
|||
|
|
</VSnackbar>
|
|||
|
|
</VLayout>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<style lang="scss">
|
|||
|
|
@import '../../../styles/chat-app.scss';
|
|||
|
|
</style>
|