2025-08-04 16:33:07 +03:30
< 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 ) ;
2025-09-28 13:46:57 +03:30
const formatRecordingTime = ( seconds ) => {
const mins = Math . floor ( seconds / 60 ) ;
const secs = seconds % 60 ;
return ` ${ mins . toString ( ) . padStart ( 2 , '0' ) } : ${ secs . toString ( ) . padStart ( 2 , '0' ) } ` ;
} ;
// Voice Recording
const isRecording = ref ( false ) ;
const mediaRecorder = ref ( null ) ;
const audioChunks = ref ( [ ] ) ;
const recordingStartTime = ref ( null ) ;
const recordingDuration = ref ( 0 ) ;
const recordingInterval = ref ( null ) ;
const voiceMessage = ref ( null ) ;
const audioStream = ref ( null ) ;
const audioContext = ref ( null ) ;
const analyser = ref ( null ) ;
const dataArray = ref ( null ) ;
const animationFrame = ref ( null ) ;
const waveformData = ref ( new Array ( 50 ) . fill ( 0 ) ) ;
const waveformCanvas = ref ( null ) ;
const canvasCtx = ref ( null ) ;
const startRecording = async ( ) => {
try {
const stream = await navigator . mediaDevices . getUserMedia ( { audio : true } ) ;
audioStream . value = stream ;
mediaRecorder . value = new MediaRecorder ( stream ) ;
audioChunks . value = [ ] ;
audioContext . value = new ( window . AudioContext || window . webkitAudioContext ) ( ) ;
analyser . value = audioContext . value . createAnalyser ( ) ;
analyser . value . smoothingTimeConstant = 0.9 ;
const source = audioContext . value . createMediaStreamSource ( stream ) ;
analyser . value . fftSize = 2048 ;
analyser . value . smoothingTimeConstant = 0.7 ;
source . connect ( analyser . value ) ;
const bufferLength = analyser . value . fftSize ;
dataArray . value = new Uint8Array ( bufferLength ) ;
mediaRecorder . value . ondataavailable = ( event ) => {
audioChunks . value . push ( event . data ) ;
} ;
mediaRecorder . value . onstop = ( ) => {
const audioBlob = new Blob ( audioChunks . value , { type : "audio/wav" } ) ;
voiceMessage . value = audioBlob ;
stopAudioAnalysis ( ) ;
} ;
mediaRecorder . value . start ( ) ;
isRecording . value = true ;
recordingStartTime . value = Date . now ( ) ;
recordingInterval . value = setInterval ( ( ) => {
recordingDuration . value = Math . floor ( ( Date . now ( ) - recordingStartTime . value ) / 1000 ) ;
} , 1000 ) ;
nextTick ( ( ) => {
if ( waveformCanvas . value ) {
canvasCtx . value = waveformCanvas . value . getContext ( "2d" ) ;
drawWaveform ( ) ;
}
} ) ;
} catch ( error ) {
console . error ( "Error accessing microphone:" , error ) ;
}
} ;
const drawWaveform = ( ) => {
if ( ! isRecording . value || ! analyser . value || ! canvasCtx . value ) return ;
analyser . value . getByteTimeDomainData ( dataArray . value ) ;
const canvas = waveformCanvas . value ;
const ctx = canvasCtx . value ;
const width = canvas . width ;
const height = canvas . height ;
ctx . clearRect ( 0 , 0 , width , height ) ;
ctx . lineWidth = 2 ;
ctx . strokeStyle = ` rgb( ${ getComputedStyle ( document . documentElement )
. getPropertyValue ( ` --v-theme- ${ activeModelColor . value } ` )
. trim ( ) } ) ` ;
ctx . beginPath ( ) ;
const sliceWidth = ( width * 1.0 ) / dataArray . value . length ;
let x = 0 ;
for ( let i = 0 ; i < dataArray . value . length ; i ++ ) {
const v = dataArray . value [ i ] / 128.0 ;
const y = ( v * height ) / 2 ;
if ( i === 0 ) {
ctx . moveTo ( x , y ) ;
} else {
ctx . lineTo ( x , y ) ;
}
x += sliceWidth ;
}
ctx . lineTo ( width , height / 2 ) ;
ctx . stroke ( ) ;
animationFrame . value = requestAnimationFrame ( drawWaveform ) ;
} ;
const animateWaveform = ( ) => {
if ( ! isRecording . value || ! analyser . value ) return ;
analyser . value . getByteFrequencyData ( dataArray . value ) ;
const chunkSize = Math . floor ( dataArray . value . length / 50 ) ;
for ( let i = 0 ; i < 50 ; i ++ ) {
let sum = 0 ;
const start = i * chunkSize ;
const end = Math . min ( start + chunkSize , dataArray . value . length ) ;
for ( let j = start ; j < end ; j ++ ) {
sum += dataArray . value [ j ] ;
}
const average = sum / ( end - start ) ;
let normalizedValue = ( average / 255 ) * 100 ;
waveformData . value [ i ] = waveformData . value [ i ] * 0.7 + normalizedValue * 0.3 ;
}
updateWaveformBars ( ) ;
animationFrame . value = requestAnimationFrame ( ( ) => animateWaveform ( ) ) ;
} ;
const updateWaveformBars = ( ) => {
const bars = document . querySelectorAll ( '.waveform-bar' ) ;
bars . forEach ( ( bar , index ) => {
if ( index < waveformData . value . length ) {
const height = Math . max ( 6 , Math . min ( 20 , 6 + ( waveformData . value [ index ] * 0.14 ) ) ) ;
bar . style . height = ` ${ height } px ` ;
}
} ) ;
} ;
const stopRecording = ( ) => {
if ( mediaRecorder . value && mediaRecorder . value . state !== 'inactive' ) {
mediaRecorder . value . stop ( ) ;
isRecording . value = false ;
if ( recordingInterval . value ) {
clearInterval ( recordingInterval . value ) ;
recordingInterval . value = null ;
}
}
} ;
const cancelRecording = ( ) => {
if ( mediaRecorder . value && mediaRecorder . value . state !== 'inactive' ) {
mediaRecorder . value . onstop = null ;
mediaRecorder . value . stop ( ) ;
}
stopAudioAnalysis ( ) ;
if ( audioStream . value ) {
audioStream . value . getTracks ( ) . forEach ( track => track . stop ( ) ) ;
audioStream . value = null ;
}
if ( recordingInterval . value ) {
clearInterval ( recordingInterval . value ) ;
recordingInterval . value = null ;
}
mediaRecorder . value = null ;
isRecording . value = false ;
voiceMessage . value = null ;
recordingDuration . value = 0 ;
recordingStartTime . value = null ;
waveformData . value = new Array ( 50 ) . fill ( 0 ) ;
audioChunks . value = [ ] ;
} ;
const stopAudioAnalysis = ( ) => {
if ( animationFrame . value ) {
cancelAnimationFrame ( animationFrame . value ) ;
animationFrame . value = null ;
}
if ( audioContext . value && audioContext . value . state !== 'closed' ) {
audioContext . value . close ( ) ;
audioContext . value = null ;
}
if ( audioStream . value ) {
audioStream . value . getTracks ( ) . forEach ( track => track . stop ( ) ) ;
audioStream . value = null ;
}
} ;
const sendVoiceMessage = async ( ) => {
if ( ! voiceMessage . value ) return ;
isQuickRepliesVisible . value = false ;
isRequestCancelled . value = false ;
if ( isInputCentered . value ) isInputCentered . value = false ;
messageQueue . value . push ( {
text : "Voice message sent" ,
sender : "user" ,
isVoiceMessage : true ,
voiceBlob : voiceMessage . value ,
duration : recordingDuration . value
} ) ;
const voiceBlob = voiceMessage . value ;
const duration = recordingDuration . value ;
voiceMessage . value = null ;
recordingDuration . value = 0 ;
await nextTick ( ) ;
scrollToBottom ( ) ;
if ( ! isProcessingQueue . value && messageQueue . value . length > 0 ) {
processMessageQueue ( ) ;
}
isBotTyping . value = true ;
abortController . value = new AbortController ( ) ;
try {
const formData = new FormData ( ) ;
formData . append ( 'voice' , voiceBlob , 'voice_message.wav' ) ;
const response = await fetch ( "/dev/voice" , {
method : "POST" ,
body : formData ,
signal : abortController . value . signal ,
} ) ;
if ( response . ok ) {
const result = await response . json ( ) ;
const responseText = result . response || result . message || result ;
const cleanText = cleanHtmlResponse ( responseText ) ;
messageQueue . value . push ( { text : cleanText , sender : "bot" } ) ;
} else {
messageQueue . value . push ( {
text : "خطا در پردازش پیام صوتی. لطفاً دوباره امتحان کنید." ,
sender : "bot" ,
} ) ;
}
} catch ( error ) {
console . error ( "Voice API Error:" , error ) ;
if ( error . name !== "AbortError" ) {
messageQueue . value . push ( {
text : "خطا در ارسال پیام صوتی. لطفاً اتصال را چک کنید." ,
sender : "bot" ,
} ) ;
}
} finally {
isBotTyping . value = false ;
}
await nextTick ( ) ;
scrollToBottom ( ) ;
} ;
const isVoiceRecordingAvailable = computed ( ( ) => {
return ! isInputDisabled . value && ! isConfirmationActive . value && ! activeForm . value && ! activeMultiForm . value ;
} ) ;
2025-08-04 16:33:07 +03:30
// 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 ;
2025-08-25 12:19:58 +03:30
if ( pendingConfirmationFormId . value !== null ) return false ;
2025-08-04 16:33:07 +03:30
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 ;
} ;
2025-09-28 13:46:57 +03:30
const rtlRegex = /[\u0600-\u06FF\u0750-\u077F]/ ;
function getDir ( str = "" ) {
return rtlRegex . test ( str ) ? "rtl" : "ltr" ;
}
2025-08-04 16:33:07 +03:30
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 ] ;
2025-08-25 12:19:58 +03:30
2025-08-04 16:33:07 +03:30
multiFormData . value [ currentStep . id ] = userResponse ;
if ( pendingConfirmationFormId . value ) {
confirmationData . value = { ... confirmationData . value , ... multiFormData . value } ;
2025-08-25 12:19:58 +03:30
return {
completed : true ,
2025-08-04 16:33:07 +03:30
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 ;
2025-09-28 13:46:57 +03:30
if ( voiceMessage . value ) {
await sendVoiceMessage ( ) ;
return ;
}
2025-08-04 16:33:07 +03:30
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 ;
2025-09-28 13:46:57 +03:30
messageQueue . value . push ( {
text : "The form has been cancelled" ,
sender : "bot" ,
} ) ;
2025-08-04 16:33:07 +03:30
msg . value = "" ;
return ;
}
isRequestCancelled . value = false ;
if ( isInputCentered . value ) isInputCentered . value = false ;
if ( selectedAttachments . value . length > 0 ) {
2025-09-28 13:46:57 +03:30
const fileNames = selectedAttachments . value . map ( ( f ) => f . name ) . join ( ", " ) ;
2025-08-04 16:33:07 +03:30
messageQueue . value . push ( {
text : userMsg || ` Files sent: ${ fileNames } ` ,
sender : "user" ,
isAttachment : true ,
2025-09-28 13:46:57 +03:30
files : [ ... selectedAttachments . value ] ,
2025-08-04 16:33:07 +03:30
} ) ;
2025-09-28 13:46:57 +03:30
2025-08-04 16:33:07 +03:30
selectedAttachments . value = [ ] ;
2025-09-28 13:46:57 +03:30
msg . value = "" ;
messageQueue . value . push ( {
text : "فایلها با موفقیت دریافت شد ✅" ,
sender : "bot" ,
isSystemMessage : true
} ) ;
await nextTick ( ) ;
scrollToBottom ( ) ;
return ;
2025-08-04 16:33:07 +03:30
} else {
messageQueue . value . push ( { text : userMsg , sender : "user" } ) ;
}
msg . value = "" ;
await nextTick ( ) ;
scrollToBottom ( ) ;
if ( ! isProcessingQueue . value && messageQueue . value . length > 0 ) {
processMessageQueue ( ) ;
}
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 ,
2025-09-28 13:46:57 +03:30
type : "multi-form" ,
2025-08-04 16:33:07 +03:30
} ) ;
}
2025-09-28 13:46:57 +03:30
return ;
2025-08-04 16:33:07 +03:30
} else if ( activeForm . value ) {
const formResponse = handleFormResponse ( userMsg ) ;
messageQueue . value . push ( { text : formResponse , sender : "bot" } ) ;
activeForm . value = null ;
2025-09-28 13:46:57 +03:30
return ;
2025-08-04 16:33:07 +03:30
} else {
const botReply = getBotReply ( userMsg ) ;
if ( botReply && typeof botReply === "object" ) {
if ( botReply . type === "multi-form" ) {
isBotTyping . value = true ;
2025-09-28 13:46:57 +03:30
try {
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" ,
} ) ;
} catch ( error ) {
console . error ( "Multi-form setup error:" , error ) ;
} finally {
isBotTyping . value = false ;
}
2025-08-04 16:33:07 +03:30
} 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 ,
2025-09-28 13:46:57 +03:30
options : form . options ,
2025-08-04 16:33:07 +03:30
} ,
2025-09-28 13:46:57 +03:30
type : "form" ,
2025-08-04 16:33:07 +03:30
} ) ;
}
return ;
} else if ( typeof botReply === "string" ) {
messageQueue . value . push ( { text : botReply , sender : "bot" } ) ;
2025-09-28 13:46:57 +03:30
return ;
2025-08-04 16:33:07 +03:30
} else {
isBotTyping . value = true ;
abortController . value = new AbortController ( ) ;
2025-09-28 13:46:57 +03:30
const SHOW _GENERIC _ERROR _IN _CHAT = true ;
2025-08-04 16:33:07 +03:30
try {
2025-09-28 13:46:57 +03:30
console . log ( "Sending message to server:" , userMsg ) ;
const response = await fetch ( "/dev/chatbot" , {
2025-08-04 16:33:07 +03:30
method : "POST" ,
2025-09-28 13:46:57 +03:30
headers : {
"Content-Type" : "application/json" ,
Accept : "application/json" ,
} ,
body : JSON . stringify ( { message : userMsg } ) ,
signal : abortController . value . signal ,
2025-08-04 16:33:07 +03:30
} ) ;
2025-09-28 13:46:57 +03:30
console . log ( "Response status:" , response . status ) ;
console . log ( "Response ok:" , response . ok ) ;
2025-08-04 16:33:07 +03:30
if ( response . ok ) {
2025-09-28 13:46:57 +03:30
const result = await response . json ( ) ;
const responseText = result . response || result . message || result ;
const cleanText = cleanHtmlResponse ( responseText ) ;
2025-08-04 16:33:07 +03:30
messageQueue . value . push ( { text : cleanText , sender : "bot" } ) ;
} else {
2025-09-28 13:46:57 +03:30
let serverText = "" ;
try {
serverText = await response . text ( ) ;
} catch ( e ) { }
console . error ( ` Server error ( ${ response . status } ) ` , serverText ) ;
if ( SHOW _GENERIC _ERROR _IN _CHAT ) {
messageQueue . value . push ( {
text : "چتبات با مشکل روبهرو شد. لطفاً دوباره امتحان کن." ,
sender : "bot" ,
} ) ;
}
return ;
}
} catch ( error ) {
console . error ( "Chatbot API Error:" , error ) ;
if ( error . name === "AbortError" ) {
console . log ( "Request was aborted" ) ;
return ;
2025-08-04 16:33:07 +03:30
}
2025-09-28 13:46:57 +03:30
if ( SHOW _GENERIC _ERROR _IN _CHAT ) {
messageQueue . value . push ( {
text : "چتبات با مشکل روبهرو شد. لطفاً اتصال را چک کن و دوباره تلاش کن." ,
sender : "bot" ,
} ) ;
2025-08-04 16:33:07 +03:30
}
2025-09-28 13:46:57 +03:30
return ;
} finally {
isBotTyping . value = false ;
2025-08-04 16:33:07 +03:30
}
}
}
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 ;
2025-08-25 12:19:58 +03:30
2025-08-04 16:33:07 +03:30
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 ] ;
2025-08-25 12:19:58 +03:30
2025-08-04 16:33:07 +03:30
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' ) ;
}
2025-09-28 13:46:57 +03:30
if ( voiceMessage . value ) {
voiceMessage . value = null ;
}
2025-08-04 16:33:07 +03:30
} ) ;
// Lifecycle
onMounted ( ( ) => {
processMessageQueue ( ) ;
} ) ;
// Expose necessary functions and refs for template usage
const quickReplies = QUICK _REPLIES ;
const models = MODELS ;
const commands = COMMANDS ;
< / script >
2025-09-28 13:46:57 +03:30
2025-08-04 16:33:07 +03:30
< 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 >
2025-09-28 13:46:57 +03:30
< div class = "pa-3 rounded-lg message-bubble bidi-text" :dir = "getDir(message.text)" : class = " [
2025-08-04 16:33:07 +03:30
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' : '' ,
2025-09-28 13:46:57 +03:30
message . isVoiceMessage ? 'voice-message-bubble' : '' ,
2025-08-04 16:33:07 +03:30
] " >
2025-09-28 13:46:57 +03:30
< div v-if = "message.isVoiceMessage" class="voice-message-display d-flex align-center gap-2" >
< VIcon icon = "tabler-microphone" size = "16" / >
< span class = "text-caption" > Voice message ( { { message . duration } } s ) < / span >
< / div >
< div v-else-if = "!message.form && !message.multiForm && !message.type" >
2025-08-04 16:33:07 +03:30
{ { 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 >
2025-09-28 13:46:57 +03:30
< div v-else-if = "message.multiForm.type === 'date'" class="form-date-input" >
< VTextField v-model = "tempDateValue" :label="message.multiForm.question" type="date"
: min = "getMinDate(message.multiForm.id)" variant = "outlined" : required = "message.multiForm.required"
@ keyup . enter = "handleDateSubmit" class = "mb-3" hide - details / >
< VBtn :color = "activeModelColor" variant = "flat" size = "small" @click ="handleDateSubmit"
: disabled = "!tempDateValue" class = "mt-2" >
Submit
< / VBtn >
< / div >
2025-08-04 16:33:07 +03:30
< / 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
2025-09-28 13:46:57 +03:30
} } < / span >
2025-08-04 16:33:07 +03:30
< / 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 ,
} " >
2025-09-28 13:46:57 +03:30
< 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);
2025-08-04 16:33:07 +03:30
-- active - model - color : var ( -- v - theme - $ { activeModelColor } ) ` ">
2025-09-28 13:46:57 +03:30
2025-08-04 16:33:07 +03:30
< 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 >
2025-09-28 13:46:57 +03:30
< div v-if = "isRecording" class="voice-recording-ui" >
< div class = "waveform-visualizer" >
< canvas ref = "waveformCanvas" > < / canvas >
< / div >
< div class = "voice-controls-wrapper" >
< div class = "recording-time" > { { formatRecordingTime ( recordingDuration ) } } < / div >
< div class = "voice-controls" >
< VBtn icon size = "small" color = "error" variant = "flat" @click ="cancelRecording" class = "voice-btn" >
< VIcon icon = "tabler-x" size = "18" / >
< / VBtn >
< VBtn icon size = "small" :color = "activeModelColor" variant = "flat" @click ="stopRecording"
class = "voice-btn" >
< VIcon icon = "tabler-check" size = "18" / >
< / VBtn >
< / div >
< / div >
2025-08-04 16:33:07 +03:30
< / div >
2025-09-28 13:46:57 +03:30
< div v-else-if = "voiceMessage" class="voice-preview-ui" >
< div class = "voice-preview-content" >
< VIcon icon = "tabler-microphone" size = "18" :color = "activeModelColor" / >
< span class = "voice-preview-text" > Voice message ready < / span >
< span class = "voice-duration" > ( { { recordingDuration } } s ) < / span >
2025-08-04 16:33:07 +03:30
< / div >
2025-09-28 13:46:57 +03:30
< div class = "voice-controls" >
< VBtn icon size = "small" color = "error" variant = "flat" @click ="cancelRecording" class = "voice-btn" >
< VIcon icon = "tabler-x" size = "18" / >
< / VBtn >
< VBtn icon size = "small" :color = "activeModelColor" variant = "flat" @click ="sendMessage" class = "voice-btn" >
< VIcon icon = "tabler-send" size = "18" / >
< / VBtn >
2025-08-04 16:33:07 +03:30
< / div >
< / div >
2025-09-28 13:46:57 +03:30
< div v-else class = "normal-input-ui" >
< 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 >
2025-08-04 16:33:07 +03:30
< / div >
2025-09-28 13:46:57 +03:30
< 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 >
2025-08-04 16:33:07 +03:30
2025-09-28 13:46:57 +03:30
< 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 >
2025-08-04 16:33:07 +03:30
2025-09-28 13:46:57 +03:30
< 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 >
2025-08-04 16:33:07 +03:30
2025-09-28 13:46:57 +03:30
< 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 >
2025-08-04 16:33:07 +03:30
2025-09-28 13:46:57 +03:30
< VBtn icon variant = "text" density = "comfortable" :color = "activeModelColor" @click ="startRecording"
: disabled = "!isVoiceRecordingAvailable" >
< VIcon icon = "tabler-microphone" / >
< / VBtn >
2025-08-04 16:33:07 +03:30
2025-09-28 13:46:57 +03:30
< 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 >
< / div >
2025-08-04 16:33:07 +03:30
2025-09-28 13:46:57 +03:30
< VBtn v-if = "!isBotTyping" icon :color="activeModelColor" @click="sendMessage" : disabled = " isInputDisabled | |
( ! msg . trim ( ) && selectedAttachments . length === 0 && ! voiceMessage )
" >
< VIcon icon = "tabler-send" > < / VIcon >
< / VBtn >
2025-08-04 16:33:07 +03:30
2025-09-28 13:46:57 +03:30
< VBtn v-else icon :color = "activeModelColor" @click ="handleStop" >
< VIcon icon = "tabler-square" > < / VIcon >
< / VBtn >
< / div >
2025-08-04 16:33:07 +03:30
< / div >
< / div >
< Transition name = "quick-replies-fade" mode = "out-in" appear >
2025-09-28 13:46:57 +03:30
< div v -if = "
isQuickRepliesVisible &&
! activeForm &&
! activeMultiForm &&
! isConfirmationActive &&
! isRecording &&
! voiceMessage &&
! isBotTyping
" class=" quick - replies - container " key=" quick - replies " >
2025-08-04 16:33:07 +03:30
< 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 ;
2025-09-28 13:46:57 +03:30
-- hover - bg - color : rgba ( var ( -- v - theme - $ { activeModelColor } ) , 0.1 ) ;
-- hover - border - color : rgba ( var ( -- v - theme - $ { activeModelColor } ) , 0.8 ) ` " @click="selectQuickReply(reply)">
2025-08-04 16:33:07 +03:30
{ { 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 , 5 MB 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 )
2025-09-28 13:46:57 +03:30
} } < / VListItemSubtitle >
2025-08-04 16:33:07 +03:30
< 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 >