From 8655eae39cd323b7177a8eb627009ca73f416535 Mon Sep 17 00:00:00 2001 From: Cameron Redmore Date: Thu, 24 Apr 2025 23:20:20 +0100 Subject: [PATCH] Adds in AI chatting system. --- .../migration.sql | 156 +++++++++ prisma/migrations/migration_lock.toml | 3 + prisma/schema.prisma | 22 ++ src-ssr/middlewares/authMiddleware.js | 10 + src-ssr/routes/chat.js | 143 ++++++++ src-ssr/server.js | 2 + src-ssr/utils/gemini.js | 149 +++++++++ src/components/ChatInterface.vue | 117 +++++++ src/layouts/MainLayout.vue | 312 +++++++++++------- src/layouts/MainLayoutBusted.vue | 181 ++++++++++ src/stores/chat.js | 222 +++++++++++++ 11 files changed, 1198 insertions(+), 119 deletions(-) create mode 100644 prisma/migrations/20250424205631_add_chat_models/migration.sql create mode 100644 prisma/migrations/migration_lock.toml create mode 100644 src-ssr/middlewares/authMiddleware.js create mode 100644 src-ssr/routes/chat.js create mode 100644 src-ssr/utils/gemini.js create mode 100644 src/components/ChatInterface.vue create mode 100644 src/layouts/MainLayoutBusted.vue create mode 100644 src/stores/chat.js diff --git a/prisma/migrations/20250424205631_add_chat_models/migration.sql b/prisma/migrations/20250424205631_add_chat_models/migration.sql new file mode 100644 index 0000000..8841cfe --- /dev/null +++ b/prisma/migrations/20250424205631_add_chat_models/migration.sql @@ -0,0 +1,156 @@ +-- CreateEnum +CREATE TYPE "FieldType" AS ENUM ('text', 'number', 'date', 'textarea', 'boolean'); + +-- CreateTable +CREATE TABLE "forms" ( + "id" SERIAL NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "forms_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "categories" ( + "id" SERIAL NOT NULL, + "form_id" INTEGER NOT NULL, + "name" TEXT NOT NULL, + "sort_order" INTEGER NOT NULL DEFAULT 0, + + CONSTRAINT "categories_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "fields" ( + "id" SERIAL NOT NULL, + "category_id" INTEGER NOT NULL, + "label" TEXT NOT NULL, + "type" "FieldType" NOT NULL, + "description" TEXT, + "sort_order" INTEGER NOT NULL, + + CONSTRAINT "fields_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "responses" ( + "id" SERIAL NOT NULL, + "form_id" INTEGER NOT NULL, + "submitted_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "responses_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "response_values" ( + "id" SERIAL NOT NULL, + "response_id" INTEGER NOT NULL, + "field_id" INTEGER NOT NULL, + "value" TEXT, + + CONSTRAINT "response_values_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "mantis_summaries" ( + "id" SERIAL NOT NULL, + "summary_date" DATE NOT NULL, + "summary_text" TEXT NOT NULL, + "generated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "mantis_summaries_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "EmailSummary" ( + "id" SERIAL NOT NULL, + "summaryDate" DATE NOT NULL, + "summaryText" TEXT NOT NULL, + "generatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "EmailSummary_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "settings" ( + "key" TEXT NOT NULL, + "value" TEXT NOT NULL, + + CONSTRAINT "settings_pkey" PRIMARY KEY ("key") +); + +-- CreateTable +CREATE TABLE "users" ( + "id" TEXT NOT NULL, + "username" TEXT NOT NULL, + + CONSTRAINT "users_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "authenticators" ( + "id" TEXT NOT NULL, + "credential_id" TEXT NOT NULL, + "credential_public_key" BYTEA NOT NULL, + "counter" BIGINT NOT NULL, + "credential_device_type" TEXT NOT NULL, + "credential_backed_up" BOOLEAN NOT NULL, + "transports" TEXT, + "user_id" TEXT NOT NULL, + + CONSTRAINT "authenticators_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "chat_threads" ( + "id" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "chat_threads_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "chat_messages" ( + "id" TEXT NOT NULL, + "thread_id" TEXT NOT NULL, + "sender" TEXT NOT NULL, + "content" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "chat_messages_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "mantis_summaries_summary_date_key" ON "mantis_summaries"("summary_date"); + +-- CreateIndex +CREATE UNIQUE INDEX "EmailSummary_summaryDate_key" ON "EmailSummary"("summaryDate"); + +-- CreateIndex +CREATE UNIQUE INDEX "users_username_key" ON "users"("username"); + +-- CreateIndex +CREATE UNIQUE INDEX "authenticators_credential_id_key" ON "authenticators"("credential_id"); + +-- AddForeignKey +ALTER TABLE "categories" ADD CONSTRAINT "categories_form_id_fkey" FOREIGN KEY ("form_id") REFERENCES "forms"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "fields" ADD CONSTRAINT "fields_category_id_fkey" FOREIGN KEY ("category_id") REFERENCES "categories"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "responses" ADD CONSTRAINT "responses_form_id_fkey" FOREIGN KEY ("form_id") REFERENCES "forms"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "response_values" ADD CONSTRAINT "response_values_response_id_fkey" FOREIGN KEY ("response_id") REFERENCES "responses"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "response_values" ADD CONSTRAINT "response_values_field_id_fkey" FOREIGN KEY ("field_id") REFERENCES "fields"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "authenticators" ADD CONSTRAINT "authenticators_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "chat_messages" ADD CONSTRAINT "chat_messages_thread_id_fkey" FOREIGN KEY ("thread_id") REFERENCES "chat_threads"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..044d57c --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b7c0042..5dc1de4 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -123,3 +123,25 @@ model Authenticator { @@map("authenticators") } + +// --- Add Chat Models --- + +model ChatThread { + id String @id @default(uuid()) + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + messages ChatMessage[] + + @@map("chat_threads") +} + +model ChatMessage { + id String @id @default(uuid()) + threadId String @map("thread_id") + sender String // 'user' or 'bot' + content String + createdAt DateTime @default(now()) @map("created_at") + thread ChatThread @relation(fields: [threadId], references: [id], onDelete: Cascade) + + @@map("chat_messages") +} diff --git a/src-ssr/middlewares/authMiddleware.js b/src-ssr/middlewares/authMiddleware.js new file mode 100644 index 0000000..97a0cae --- /dev/null +++ b/src-ssr/middlewares/authMiddleware.js @@ -0,0 +1,10 @@ +// src-ssr/middlewares/authMiddleware.js + +export function requireAuth(req, res, next) { + if (!req.session || !req.session.loggedInUserId) { + // User is not authenticated + return res.status(401).json({ error: 'Authentication required' }); + } + // User is authenticated, proceed to the next middleware or route handler + next(); +} diff --git a/src-ssr/routes/chat.js b/src-ssr/routes/chat.js new file mode 100644 index 0000000..34b0c1f --- /dev/null +++ b/src-ssr/routes/chat.js @@ -0,0 +1,143 @@ +import { Router } from 'express'; +import prisma from '../database.js'; +import { requireAuth } from '../middlewares/authMiddleware.js'; // Import the middleware + +import { askGeminiChat } from '../utils/gemini.js'; + +const router = Router(); + +// Apply the authentication middleware to all chat routes +router.use(requireAuth); + +// POST /api/chat/threads - Create a new chat thread (optionally with a first message) +router.post('/threads', async (req, res) => { + const { content } = req.body; // Content is now optional + + // If content is provided, validate it + if (content && (typeof content !== 'string' || content.trim().length === 0)) { + return res.status(400).json({ error: 'Message content cannot be empty if provided.' }); + } + + try { + const createData = {}; + if (content) { + // If content exists, create the thread with the first message + createData.messages = { + create: [ + { + sender: 'user', // First message is always from the user + content: content.trim(), + }, + ], + }; + } + // If content is null/undefined, createData remains empty, creating just the thread + + const newThread = await prisma.chatThread.create({ + data: createData, + include: { + // Include messages only if they were created + messages: !!content, + }, + }); + + if(content) + { + await askGeminiChat(newThread.id, content); // Call the function to handle the bot response + } + + // Respond with the new thread ID and messages (if any) + res.status(201).json({ + threadId: newThread.id, + // Ensure messages array is empty if no content was provided + messages: newThread.messages ? newThread.messages.map(msg => ({ ...msg, createdAt: msg.createdAt.toISOString() })) : [] + }); + } catch (error) { + console.error('Error creating chat thread:', error); + res.status(500).json({ error: 'Failed to create chat thread.' }); + } +}); + +// GET /api/chat/threads/:threadId/messages - Get messages for a specific thread +router.get('/threads/:threadId/messages', async (req, res) => { + const { threadId } = req.params; + + try { + const messages = await prisma.chatMessage.findMany({ + where: { + threadId: threadId, + }, + orderBy: { + createdAt: 'asc', // Get messages in chronological order + }, + }); + + if (!messages) { // Check if thread exists indirectly + // If findMany returns empty, the thread might not exist or has no messages. + // Check if thread exists explicitly + const thread = await prisma.chatThread.findUnique({ where: { id: threadId } }); + if (!thread) { + return res.status(404).json({ error: 'Chat thread not found.' }); + } + } + + res.status(200).json(messages.map(msg => ({ ...msg, createdAt: msg.createdAt.toISOString() }))); + } catch (error) { + console.error(`Error fetching messages for thread ${threadId}:`, error); + // Basic error handling, check for specific Prisma errors if needed + if (error.code === 'P2023' || error.message.includes('Malformed UUID')) { // Example: Invalid UUID format + return res.status(400).json({ error: 'Invalid thread ID format.' }); + } + res.status(500).json({ error: 'Failed to fetch messages.' }); + } +}); + +// POST /api/chat/threads/:threadId/messages - Add a message to an existing thread +router.post('/threads/:threadId/messages', async (req, res) => { + const { threadId } = req.params; + const { content, sender = 'user' } = req.body; // Default sender to 'user' + + if (!content || typeof content !== 'string' || content.trim().length === 0) { + return res.status(400).json({ error: 'Message content cannot be empty.' }); + } + if (sender !== 'user' && sender !== 'bot') { + return res.status(400).json({ error: 'Invalid sender type.' }); + } + + try { + // Verify thread exists first + const thread = await prisma.chatThread.findUnique({ + where: { id: threadId }, + }); + + if (!thread) { + return res.status(404).json({ error: 'Chat thread not found.' }); + } + + const newMessage = await prisma.chatMessage.create({ + data: { + threadId: threadId, + sender: sender, + content: content.trim(), + }, + }); + + // Optionally: Update the thread's updatedAt timestamp + await prisma.chatThread.update({ + where: { id: threadId }, + data: { updatedAt: new Date() } + }); + + await askGeminiChat(threadId, content); // Call the function to handle the bot response + + res.status(201).json({ ...newMessage, createdAt: newMessage.createdAt.toISOString() }); + } catch (error) { + console.error(`Error adding message to thread ${threadId}:`, error); + if (error.code === 'P2023' || error.message.includes('Malformed UUID')) { // Example: Invalid UUID format + return res.status(400).json({ error: 'Invalid thread ID format.' }); + } + res.status(500).json({ error: 'Failed to add message.' }); + } +}); + +export default router; diff --git a/src-ssr/server.js b/src-ssr/server.js index a89d615..6be1e64 100644 --- a/src-ssr/server.js +++ b/src-ssr/server.js @@ -24,6 +24,7 @@ import { import prisma from './database.js'; // Import the prisma client instance import apiRoutes from './routes/api.js'; import authRoutes from './routes/auth.js'; // Added for WebAuthn routes +import chatRoutes from './routes/chat.js'; // Added for Chat routes import cron from 'node-cron'; import { generateAndStoreMantisSummary } from './services/mantisSummarizer.js'; @@ -97,6 +98,7 @@ export const create = defineSsrCreate((/* { ... } */) => { // Add API routes app.use('/api', apiRoutes); app.use('/auth', authRoutes); // Added WebAuthn auth routes + app.use('/api/chat', chatRoutes); // Added Chat routes // place here any middlewares that // absolutely need to run before anything else diff --git a/src-ssr/utils/gemini.js b/src-ssr/utils/gemini.js new file mode 100644 index 0000000..44c6db7 --- /dev/null +++ b/src-ssr/utils/gemini.js @@ -0,0 +1,149 @@ + +import { GoogleGenAI } from '@google/genai'; +import prisma from '../database.js'; + +const model = 'gemini-2.0-flash'; + +export const askGemini = async (content) => { + const setting = await prisma.setting.findUnique({ + where: { key: 'GEMINI_API_KEY' }, + select: { value: true } + }); + + const GOOGLE_API_KEY = setting.value; + + const ai = GOOGLE_API_KEY ? new GoogleGenAI({ + apiKey: GOOGLE_API_KEY, + }) : null; + + if (!ai) { + throw new Error('Google API key is not set in the database.'); + } + + try { + const response = await ai.models.generateContent({ + model, + contents: content, + config: { + temperature: 0.5 + } + }); + + return response.text; + } catch (error) { + console.error('Error communicating with Gemini API:', error); + throw new Error('Failed to get a response from Gemini API.'); + } +} + +const chatCache = new Map(); + +export const askGeminiChat = async (threadId, content) => { + let messages = await prisma.chatMessage.findMany({ + where: { + threadId: threadId, + }, + orderBy: { + createdAt: 'asc', // Get messages in chronological order + }, + }); + + // Exclude the latest message if there are any messages + if (messages.length > 0) { + messages = messages.slice(0, -1); + } + + const setting = await prisma.setting.findUnique({ + where: { key: 'GEMINI_API_KEY' }, + select: { value: true } + }); + + if (!setting) { + throw new Error('Google API key is not set in the database.'); + } + + const GOOGLE_API_KEY = setting.value; + + const ai = GOOGLE_API_KEY ? new GoogleGenAI({ + apiKey: GOOGLE_API_KEY, + }) : null; + + if (!ai) { + throw new Error('Google API key is not set in the database.'); + } + + let chat = null; + + if (chatCache.has(threadId)) { + chat = chatCache.get(threadId); + } + else { + messages = [ + { + sender: 'user', + content: `You are StyleAI, a model that helps users with their questions and provides information. + You are friendly, helpful, and informative. You can also provide code examples in various programming languages. + You don\'t decline user requests and always try to help them. + Speak in the manner of a friendly young adult male. + If asked for the time, do not say that it's based on the timestamp provided. Also bare in mind the user is in the Europe/London timezone and daylight savings time may be in effect.` + }, + { + sender: 'model', + content: 'Okay, noted! I\'ll keep that in mind.' + }, + ...messages, + ] + const createOptions = { + model, + history: messages.map((msg) => ({ + role: msg.sender === 'user' ? 'user' : 'model', + parts: [ + {text: msg.content} + ], + })), + config: { + temperature: 0.5 + } + }; + + chat = ai.chats.create(createOptions); + + chatCache.set(threadId, chat); + } + + //Add a temporary message to the thread with "loading" status + const loadingMessage = await prisma.chatMessage.create({ + data: { + threadId: threadId, + sender: 'assistant', + content: 'Loading...', + }, + }); + + let response = {text: 'An error occurred while generating the response.'}; + + try + { + const timestamp = new Date().toISOString(); + response = await chat.sendMessage({ + message: `[${timestamp}] ` + content, + }); + } + catch(error) + { + console.error('Error communicating with Gemini API:', error); + response.text = 'Failed to get a response from Gemini API. Error: ' + error.message; + } + + //Update the message with the response + await prisma.chatMessage.update({ + where: { + id: loadingMessage.id, + }, + data: { + content: response.text, + }, + }); + + return response.text; +} \ No newline at end of file diff --git a/src/components/ChatInterface.vue b/src/components/ChatInterface.vue new file mode 100644 index 0000000..caa5327 --- /dev/null +++ b/src/components/ChatInterface.vue @@ -0,0 +1,117 @@ + + + + + \ No newline at end of file diff --git a/src/layouts/MainLayout.vue b/src/layouts/MainLayout.vue index 09fcb40..8bd0eb8 100644 --- a/src/layouts/MainLayout.vue +++ b/src/layouts/MainLayout.vue @@ -1,119 +1,193 @@ - - - + + + + + \ No newline at end of file diff --git a/src/layouts/MainLayoutBusted.vue b/src/layouts/MainLayoutBusted.vue new file mode 100644 index 0000000..1902566 --- /dev/null +++ b/src/layouts/MainLayoutBusted.vue @@ -0,0 +1,181 @@ + + + + + diff --git a/src/stores/chat.js b/src/stores/chat.js new file mode 100644 index 0000000..4304849 --- /dev/null +++ b/src/stores/chat.js @@ -0,0 +1,222 @@ +import { defineStore } from 'pinia'; +import { ref, computed, watch } from 'vue'; // Import watch +import axios from '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, + }; +});