256 lines
7.5 KiB
JavaScript
256 lines
7.5 KiB
JavaScript
import { defineStore } from 'pinia';
|
|
import { ref, computed, watch } from 'vue'; // Import watch
|
|
import axios from 'boot/axios';
|
|
|
|
export const useChatStore = defineStore('chat', () =>
|
|
{
|
|
const isVisible = ref(false);
|
|
const currentThreadId = ref(null);
|
|
const messages = ref([]); // Array of { sender: 'user' | 'bot', content: string, createdAt?: Date, loading?: boolean }
|
|
const isLoading = ref(false);
|
|
const error = ref(null);
|
|
const pollingIntervalId = ref(null); // To store the interval ID
|
|
|
|
// --- Getters ---
|
|
const chatMessages = computed(() => messages.value);
|
|
const isChatVisible = computed(() => isVisible.value);
|
|
const activeThreadId = computed(() => currentThreadId.value);
|
|
|
|
// --- Actions ---
|
|
|
|
// New action to create a thread if it doesn't exist
|
|
async function createThreadIfNotExists()
|
|
{
|
|
if (currentThreadId.value) return; // Already have a thread
|
|
|
|
isLoading.value = true;
|
|
error.value = null;
|
|
try
|
|
{
|
|
// Call the endpoint without content to just create the thread
|
|
const response = await axios.post('/api/chat/threads', {});
|
|
currentThreadId.value = response.data.threadId;
|
|
messages.value = []; // Start with an empty message list for the new thread
|
|
console.log('Created new chat thread:', currentThreadId.value);
|
|
// Start polling now that we have a thread ID
|
|
startPolling();
|
|
}
|
|
catch (err)
|
|
{
|
|
console.error('Error creating chat thread:', err);
|
|
error.value = 'Failed to start chat.';
|
|
// Don't set isVisible to false, let the user see the error
|
|
}
|
|
finally
|
|
{
|
|
isLoading.value = false;
|
|
}
|
|
}
|
|
|
|
function toggleChat()
|
|
{
|
|
isVisible.value = !isVisible.value;
|
|
|
|
if (isVisible.value)
|
|
{
|
|
if (!currentThreadId.value)
|
|
{
|
|
// If opening and no thread exists, create one
|
|
createThreadIfNotExists();
|
|
}
|
|
else
|
|
{
|
|
// If opening and thread exists, fetch messages if empty and start polling
|
|
if (messages.value.length === 0)
|
|
{
|
|
fetchMessages();
|
|
}
|
|
startPolling();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// If closing, stop polling
|
|
stopPolling();
|
|
}
|
|
}
|
|
|
|
async function fetchMessages()
|
|
{
|
|
if (!currentThreadId.value)
|
|
{
|
|
console.log('No active thread to fetch messages for.');
|
|
// Don't try to fetch if no thread ID yet. createThreadIfNotExists handles the initial state.
|
|
return;
|
|
}
|
|
// Avoid setting isLoading if polling, maybe use a different flag? For now, keep it simple.
|
|
// isLoading.value = true; // Might cause flickering during polling
|
|
error.value = null; // Clear previous errors on fetch attempt
|
|
try
|
|
{
|
|
const response = await axios.get(`/api/chat/threads/${currentThreadId.value}/messages`);
|
|
const newMessages = response.data.map(msg => ({
|
|
sender: msg.sender,
|
|
content: msg.content,
|
|
createdAt: new Date(msg.createdAt),
|
|
loading: msg.content === 'Loading...'
|
|
})).sort((a, b) => a.createdAt - b.createdAt);
|
|
|
|
// Only update if messages have actually changed to prevent unnecessary re-renders
|
|
if (JSON.stringify(messages.value) !== JSON.stringify(newMessages))
|
|
{
|
|
messages.value = newMessages;
|
|
}
|
|
|
|
}
|
|
catch (err)
|
|
{
|
|
console.error('Error fetching messages:', err);
|
|
error.value = 'Failed to load messages.';
|
|
// Don't clear messages on polling error, keep the last known state
|
|
// messages.value = [];
|
|
stopPolling(); // Stop polling if there's an error fetching
|
|
}
|
|
finally
|
|
{
|
|
// isLoading.value = false;
|
|
}
|
|
}
|
|
|
|
// Function to start polling
|
|
function startPolling()
|
|
{
|
|
if (pollingIntervalId.value) return; // Already polling
|
|
if (!currentThreadId.value) return; // No thread to poll for
|
|
|
|
console.log('Starting chat polling for thread:', currentThreadId.value);
|
|
pollingIntervalId.value = setInterval(fetchMessages, 5000); // Poll every 5 seconds
|
|
}
|
|
|
|
// Function to stop polling
|
|
function stopPolling()
|
|
{
|
|
if (pollingIntervalId.value)
|
|
{
|
|
console.log('Stopping chat polling.');
|
|
clearInterval(pollingIntervalId.value);
|
|
pollingIntervalId.value = null;
|
|
}
|
|
}
|
|
|
|
|
|
async function sendMessage(content)
|
|
{
|
|
if (!content.trim()) return;
|
|
if (!currentThreadId.value)
|
|
{
|
|
error.value = 'Cannot send message: No active chat thread.';
|
|
console.error('Attempted to send message without a thread ID.');
|
|
return; // Should not happen if UI waits for thread creation
|
|
}
|
|
|
|
const userMessage = {
|
|
sender: 'user',
|
|
content: content.trim(),
|
|
createdAt: new Date(),
|
|
};
|
|
messages.value.push(userMessage);
|
|
|
|
const loadingMessage = { sender: 'bot', content: '...', loading: true, createdAt: new Date(Date.now() + 1) }; // Ensure unique key/time
|
|
messages.value.push(loadingMessage);
|
|
|
|
// Stop polling temporarily while sending a message to avoid conflicts
|
|
stopPolling();
|
|
|
|
isLoading.value = true; // Indicate activity
|
|
error.value = null;
|
|
|
|
try
|
|
{
|
|
const payload = { content: userMessage.content };
|
|
// Always post to the existing thread once it's created
|
|
const response = await axios.post(`/api/chat/threads/${currentThreadId.value}/messages`, payload);
|
|
|
|
// Remove loading indicator
|
|
messages.value = messages.value.filter(m => !m.loading);
|
|
|
|
// The POST might return the new message, but we'll rely on the next fetchMessages call
|
|
// triggered by startPolling to get the latest state including any potential bot response.
|
|
|
|
// Immediately fetch messages after sending to get the updated list
|
|
await fetchMessages();
|
|
|
|
}
|
|
catch (err)
|
|
{
|
|
console.error('Error sending message:', err);
|
|
error.value = 'Failed to send message.';
|
|
// Remove loading indicator on error
|
|
messages.value = messages.value.filter(m => !m.loading);
|
|
// Optionally add an error message to the chat
|
|
// Ensure the object is correctly formatted
|
|
messages.value.push({ sender: 'bot', content: "Sorry, I couldn't send that message.", createdAt: new Date() });
|
|
}
|
|
finally
|
|
{
|
|
isLoading.value = false;
|
|
// Restart polling after sending attempt is complete
|
|
startPolling();
|
|
}
|
|
}
|
|
|
|
// Call this when the user logs out or the app closes if you want to clear state
|
|
function resetChat()
|
|
{
|
|
stopPolling(); // Ensure polling stops on reset
|
|
isVisible.value = false;
|
|
currentThreadId.value = null;
|
|
messages.value = [];
|
|
isLoading.value = false;
|
|
error.value = null;
|
|
}
|
|
|
|
// Watch for visibility changes to manage polling (alternative to putting logic in toggleChat)
|
|
// watch(isVisible, (newValue) => {
|
|
// if (newValue && currentThreadId.value) {
|
|
// startPolling();
|
|
// } else {
|
|
// stopPolling();
|
|
// }
|
|
// });
|
|
|
|
// Watch for thread ID changes (e.g., after creation)
|
|
// watch(currentThreadId, (newId) => {
|
|
// if (newId && isVisible.value) {
|
|
// messages.value = []; // Clear old messages if any
|
|
// fetchMessages(); // Fetch messages for the new thread
|
|
// startPolling(); // Start polling for the new thread
|
|
// } else {
|
|
// stopPolling(); // Stop polling if thread ID becomes null
|
|
// }
|
|
// });
|
|
|
|
|
|
return {
|
|
// State refs
|
|
isVisible,
|
|
currentThreadId,
|
|
messages,
|
|
isLoading,
|
|
error,
|
|
|
|
// Computed getters
|
|
chatMessages,
|
|
isChatVisible,
|
|
activeThreadId,
|
|
|
|
// Actions
|
|
toggleChat,
|
|
sendMessage,
|
|
fetchMessages, // Expose if needed externally
|
|
resetChat,
|
|
// Expose polling control if needed externally, though typically managed internally
|
|
// startPolling,
|
|
// stopPolling,
|
|
};
|
|
});
|