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 @@
-
-
-
-
-
-
-
-
-
- StylePoint
-
-
-
-
-
-
- {{ item.meta.title }}
-
-
-
-
-
- {{ item.meta.title }}
- {{ item.meta.caption }}
-
-
-
-
-
-
- Logout
-
-
-
-
-
- Logout
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+ StylePoint
+
+
+
+
+
+
+ {{ item.meta.title }}
+
+
+
+
+
+ {{ item.meta.title }}
+ {{ item.meta.caption }}
+
+
+
+
+
+
+ Logout
+
+
+
+
+
+ Logout
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Chat
+
+
+
+
+
+
+
+
+
+
+
+ {{ chatError }}
+
+
+
+
+
+
+
+
+
+
+
+
\ 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 @@
+
+
+
+
+
+
+
+
+
+ StylePoint
+
+
+
+
+
+ Forms
+
+
+
+
+
+ Forms
+ View existing forms
+
+
+
+
+
+ Mantis Summaries
+
+
+
+
+
+ Mantis Summaries
+ View daily summaries
+
+
+
+
+
+ Email Summaries
+
+
+
+
+
+ Email Summaries
+ View email summaries
+
+
+
+
+
+ Settings
+
+
+
+
+
+
+ Settings
+ Manage application settings
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Chat
+
+
+
+
+
+
+
+
+
+
+
+ {{ chatError }}
+
+
+
+
+
+
+
+
+
+
+
+
+
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,
+ };
+});