Moved away from SSR to regular Node API server.

This commit is contained in:
Cameron Redmore 2025-04-25 12:50:44 +01:00
parent 9aea69c7be
commit 83d93aefc0
30 changed files with 939 additions and 1024 deletions

View file

@ -3,5 +3,13 @@
</template>
<script setup>
//
import { useAuthStore } from './stores/auth';
defineOptions({
preFetch()
{
const authStore = useAuthStore();
return authStore.checkAuthStatus();
}
});
</script>

14
src/boot/axios.js Normal file
View file

@ -0,0 +1,14 @@
import { boot } from 'quasar/wrappers';
import axios from 'axios';
// Be careful when using SSR for cross-request state pollution
// due to creating a Singleton instance here;
// If any client changes this (global) instance, it might be a
// good idea to move this instance creation inside of the
// "export default () => {}" function below (which runs individually
// for each client)
axios.defaults.withCredentials = true; // Enable sending cookies with requests
// Export the API instance so you can import it easily elsewhere, e.g. stores
export default axios;

View file

@ -7,17 +7,17 @@
:model-value="true"
>
<q-list>
<q-item
clickable
v-ripple
@click="toggleLeftDrawer"
<q-item
clickable
v-ripple
@click="toggleLeftDrawer"
>
<q-item-section avatar>
<q-icon name="menu" />
</q-item-section>
<q-item-section>
<q-item-label class="text-h6">
StylePoint
<q-item-label class="text-h6">
StylePoint
</q-item-label>
</q-item-section>
</q-item>
@ -31,9 +31,9 @@
:to="{ name: item.name }"
exact
>
<q-tooltip
anchor="center right"
self="center left"
<q-tooltip
anchor="center right"
self="center left"
>
<span>{{ item.meta.title }}</span>
</q-tooltip>
@ -42,8 +42,8 @@
</q-item-section>
<q-item-section>
<q-item-label>{{ item.meta.title }}</q-item-label>
<q-item-label caption>
{{ item.meta.caption }}
<q-item-label caption>
{{ item.meta.caption }}
</q-item-label>
</q-item-section>
</q-item>
@ -55,9 +55,9 @@
v-ripple
@click="logout"
>
<q-tooltip
anchor="center right"
self="center left"
<q-tooltip
anchor="center right"
self="center left"
>
<span>Logout</span>
</q-tooltip>
@ -67,7 +67,7 @@
<q-item-section>
<q-item-label>Logout</q-item-label>
</q-item-section>
</q-item>
</q-item>
</q-list>
</q-drawer>
@ -76,10 +76,10 @@
</q-page-container>
<!-- Chat FAB -->
<q-page-sticky
v-if="isAuthenticated"
position="bottom-right"
:offset="[18, 18]"
<q-page-sticky
v-if="isAuthenticated"
position="bottom-right"
:offset="[18, 18]"
>
<q-fab
v-model="fabOpen"
@ -92,28 +92,28 @@
</q-page-sticky>
<!-- Chat Window Dialog -->
<q-dialog
v-model="isChatVisible"
:maximized="$q.screen.lt.sm"
fixed
persistent
style="width: max(400px, 25%);"
<q-dialog
v-model="isChatVisible"
:maximized="$q.screen.lt.sm"
fixed
persistent
style="width: max(400px, 25%);"
>
<q-card style="width: max(400px, 25%); height: 600px; max-height: 80vh;">
<q-bar class="bg-primary text-white">
<div>Chat</div>
<q-space />
<q-btn
dense
flat
icon="close"
@click="toggleChat"
<q-btn
dense
flat
icon="close"
@click="toggleChat"
/>
</q-bar>
<q-card-section
class="q-pa-none"
style="height: calc(100% - 50px);"
<q-card-section
class="q-pa-none"
style="height: calc(100% - 50px);"
>
<ChatInterface
:messages="chatMessages"
@ -121,23 +121,23 @@
/>
</q-card-section>
<q-inner-loading :showing="isLoading">
<q-spinner-gears
size="50px"
color="primary"
<q-spinner-gears
size="50px"
color="primary"
/>
</q-inner-loading>
<q-banner
v-if="chatError"
inline-actions
class="text-white bg-red"
<q-banner
v-if="chatError"
inline-actions
class="text-white bg-red"
>
{{ chatError }}
<template #action>
<q-btn
flat
color="white"
label="Dismiss"
@click="clearError"
<q-btn
flat
color="white"
label="Dismiss"
@click="clearError"
/>
</template>
</q-banner>
@ -147,7 +147,7 @@
</template>
<script setup>
import axios from 'axios';
import axios from 'boot/axios';
import { ref, computed } from 'vue'; // Import computed
import { useRouter } from 'vue-router';
import { useQuasar } from 'quasar';
@ -175,9 +175,9 @@ const isAuthenticated = computed(() => authStore.isAuthenticated); // Get auth s
const mainLayoutRoutes = routes.find(r => r.path === '/')?.children || [];
// Compute navigation items based on auth state and route meta
const navItems = computed(() =>
const navItems = computed(() =>
{
return mainLayoutRoutes.filter(route =>
return mainLayoutRoutes.filter(route =>
{
const navGroup = route.meta?.navGroup;
if (!navGroup) return false; // Only include routes with navGroup defined
@ -192,41 +192,41 @@ const navItems = computed(() =>
// Method to toggle chat visibility via the store action
const toggleChat = () =>
const toggleChat = () =>
{
// Optional: Add an extra check here if needed, though hiding the button is primary
if (isAuthenticated.value)
if (isAuthenticated.value)
{
chatStore.toggleChat();
}
};
// Method to send a message via the store action
const handleSendMessage = (messageContent) =>
const handleSendMessage = (messageContent) =>
{
chatStore.sendMessage(messageContent);
};
// Method to clear errors in the store (optional)
const clearError = () =>
const clearError = () =>
{
chatStore.error = null; // Directly setting ref or add an action in store
};
function toggleLeftDrawer()
function toggleLeftDrawer()
{
leftDrawerOpen.value = !leftDrawerOpen.value;
}
async function logout()
async function logout()
{
try
try
{
await axios.post('/auth/logout');
await axios.post('/api/auth/logout');
authStore.logout(); // Use the store action to update state
// No need to manually push, router guard should redirect
// router.push({ name: 'login' });
}
catch (error)
}
catch (error)
{
console.error('Logout failed:', error);

View file

@ -1,136 +1,136 @@
<template>
<q-page padding>
<div class="text-h4 q-mb-md">
Create New Form
<div class="text-h4 q-mb-md">
Create New Form
</div>
<q-form
@submit.prevent="createForm"
class="q-gutter-md"
<q-form
@submit.prevent="createForm"
class="q-gutter-md"
>
<q-input
outlined
v-model="form.title"
label="Form Title *"
<q-input
outlined
v-model="form.title"
label="Form Title *"
lazy-rules
:rules="[val => val && val.length > 0 || 'Please enter a title']"
:rules="[val => val && val.length > 0 || 'Please enter a title']"
/>
<q-input
outlined
v-model="form.description"
label="Form Description"
type="textarea"
autogrow
<q-input
outlined
v-model="form.description"
label="Form Description"
type="textarea"
autogrow
/>
<q-separator class="q-my-lg" />
<div class="text-h6 q-mb-sm">
Categories & Fields
<div class="text-h6 q-mb-sm">
Categories & Fields
</div>
<div
v-for="(category, catIndex) in form.categories"
<div
v-for="(category, catIndex) in form.categories"
:key="catIndex"
class="q-mb-lg q-pa-md bordered rounded-borders"
class="q-mb-lg q-pa-md bordered rounded-borders"
>
<div class="row items-center q-mb-sm">
<q-input
outlined
dense
v-model="category.name"
<q-input
outlined
dense
v-model="category.name"
:label="`Category ${catIndex + 1} Name *`"
class="col q-mr-sm"
class="col q-mr-sm"
lazy-rules
:rules="[val => val && val.length > 0 || 'Category name required']"
:rules="[val => val && val.length > 0 || 'Category name required']"
/>
<q-btn
flat
round
dense
icon="delete"
color="negative"
<q-btn
flat
round
dense
icon="delete"
color="negative"
@click="removeCategory(catIndex)"
title="Remove Category"
title="Remove Category"
/>
</div>
<div
v-for="(field, fieldIndex) in category.fields"
<div
v-for="(field, fieldIndex) in category.fields"
:key="fieldIndex"
class="q-ml-md q-mb-sm field-item"
class="q-ml-md q-mb-sm field-item"
>
<div class="row items-center q-gutter-sm">
<q-input
outlined
dense
v-model="field.label"
label="Field Label *"
class="col"
<q-input
outlined
dense
v-model="field.label"
label="Field Label *"
class="col"
lazy-rules
:rules="[val => val && val.length > 0 || 'Field label required']"
:rules="[val => val && val.length > 0 || 'Field label required']"
/>
<q-select
outlined
dense
v-model="field.type"
:options="fieldTypes"
<q-select
outlined
dense
v-model="field.type"
:options="fieldTypes"
label="Field Type *"
class="col-auto"
style="min-width: 150px;"
class="col-auto"
style="min-width: 150px;"
lazy-rules
:rules="[val => !!val || 'Field type required']"
:rules="[val => !!val || 'Field type required']"
/>
<q-btn
flat
round
dense
icon="delete"
<q-btn
flat
round
dense
icon="delete"
color="negative"
@click="removeField(catIndex, fieldIndex)"
title="Remove Field"
@click="removeField(catIndex, fieldIndex)"
title="Remove Field"
/>
</div>
<q-input
v-model="field.description"
outlined
dense
label="Field Description (Optional)"
<q-input
v-model="field.description"
outlined
dense
label="Field Description (Optional)"
autogrow
class="q-mt-xs q-mb-xl"
hint="This description will appear below the field label on the form."
class="q-mt-xs q-mb-xl"
hint="This description will appear below the field label on the form."
/>
</div>
<q-btn
color="primary"
label="Add Field"
@click="addField(catIndex)"
class="q-ml-md q-mt-sm"
<q-btn
color="primary"
label="Add Field"
@click="addField(catIndex)"
class="q-ml-md q-mt-sm"
/>
</div>
<q-btn
color="secondary"
label="Add Category"
@click="addCategory"
<q-btn
color="secondary"
label="Add Category"
@click="addCategory"
/>
<q-separator class="q-my-lg" />
<div>
<q-btn
label="Create Form"
type="submit"
color="primary"
:loading="submitting"
<q-btn
label="Create Form"
type="submit"
color="primary"
:loading="submitting"
/>
<q-btn
label="Cancel"
type="reset"
color="warning"
class="q-ml-sm"
:to="{ name: 'formList' }"
<q-btn
label="Cancel"
type="reset"
color="warning"
class="q-ml-sm"
:to="{ name: 'formList' }"
/>
</div>
</q-form>
@ -139,7 +139,7 @@
<script setup>
import { ref } from 'vue';
import axios from 'axios';
import axios from 'boot/axios';
import { useQuasar } from 'quasar';
import { useRouter } from 'vue-router';
@ -157,30 +157,30 @@ const form = ref({
const fieldTypes = ref(['text', 'number', 'date', 'textarea', 'boolean']);
const submitting = ref(false);
function addCategory()
function addCategory()
{
form.value.categories.push({ name: `Category ${form.value.categories.length + 1}`, fields: [{ label: '', type: null, description: '' }] });
}
function removeCategory(index)
function removeCategory(index)
{
form.value.categories.splice(index, 1);
}
function addField(catIndex)
function addField(catIndex)
{
form.value.categories[catIndex].fields.push({ label: '', type: 'text', description: '' });
}
function removeField(catIndex, fieldIndex)
function removeField(catIndex, fieldIndex)
{
form.value.categories[catIndex].fields.splice(fieldIndex, 1);
}
async function createForm()
async function createForm()
{
submitting.value = true;
try
try
{
const response = await axios.post('/api/forms', form.value);
$q.notify({
@ -190,8 +190,8 @@ async function createForm()
icon: 'check_circle'
});
router.push({ name: 'formList' });
}
catch (error)
}
catch (error)
{
console.error('Error creating form:', error);
const message = error.response?.data?.error || 'Failed to create form. Please check the details and try again.';
@ -201,8 +201,8 @@ async function createForm()
message: message,
icon: 'report_problem'
});
}
finally
}
finally
{
submitting.value = false;
}

View file

@ -1,13 +1,13 @@
<template>
<q-page padding>
<div class="text-h4 q-mb-md">
Edit Form
<div class="text-h4 q-mb-md">
Edit Form
</div>
<q-form
v-if="!loading && form"
@submit.prevent="updateForm"
class="q-gutter-md"
<q-form
v-if="!loading && form"
@submit.prevent="updateForm"
class="q-gutter-md"
>
<q-input
outlined
@ -27,18 +27,18 @@
<q-separator class="q-my-lg" />
<div class="text-h6 q-mb-sm">
Categories & Fields
<div class="text-h6 q-mb-sm">
Categories & Fields
</div>
<div
v-for="(category, catIndex) in form.categories"
:key="category.id || catIndex"
class="q-mb-lg q-pa-md bordered rounded-borders"
<div
v-for="(category, catIndex) in form.categories"
:key="category.id || catIndex"
class="q-mb-lg q-pa-md bordered rounded-borders"
>
<div class="row items-center q-mb-sm">
<q-input
outlined
outlined
dense
v-model="category.name"
:label="`Category ${catIndex + 1} Name *`"
@ -46,25 +46,25 @@
lazy-rules
:rules="[ val => val && val.length > 0 || 'Category name required']"
/>
<q-btn
flat
round
dense
icon="delete"
color="negative"
@click="removeCategory(catIndex)"
title="Remove Category"
<q-btn
flat
round
dense
icon="delete"
color="negative"
@click="removeCategory(catIndex)"
title="Remove Category"
/>
</div>
<div
v-for="(field, fieldIndex) in category.fields"
:key="field.id || fieldIndex"
class="q-ml-md q-mb-sm"
<div
v-for="(field, fieldIndex) in category.fields"
:key="field.id || fieldIndex"
class="q-ml-md q-mb-sm"
>
<div class="row items-center q-gutter-sm">
<q-input
outlined
outlined
dense
v-model="field.label"
label="Field Label *"
@ -73,7 +73,7 @@
:rules="[ val => val && val.length > 0 || 'Field label required']"
/>
<q-select
outlined
outlined
dense
v-model="field.type"
:options="fieldTypes"
@ -83,14 +83,14 @@
lazy-rules
:rules="[ val => !!val || 'Field type required']"
/>
<q-btn
flat
round
dense
icon="delete"
color="negative"
@click="removeField(catIndex, fieldIndex)"
title="Remove Field"
<q-btn
flat
round
dense
icon="delete"
color="negative"
@click="removeField(catIndex, fieldIndex)"
title="Remove Field"
/>
</div>
<q-input
@ -103,52 +103,52 @@
hint="This description will appear below the field label on the form."
/>
</div>
<q-btn
outline
color="primary"
label="Add Field"
@click="addField(catIndex)"
class="q-ml-md q-mt-sm"
<q-btn
outline
color="primary"
label="Add Field"
@click="addField(catIndex)"
class="q-ml-md q-mt-sm"
/>
</div>
<q-btn
outline
color="secondary"
label="Add Category"
@click="addCategory"
<q-btn
outline
color="secondary"
label="Add Category"
@click="addCategory"
/>
<q-separator class="q-my-lg" />
<div>
<q-btn
outline
label="Update Form"
type="submit"
color="primary"
:loading="submitting"
<q-btn
outline
label="Update Form"
type="submit"
color="primary"
:loading="submitting"
/>
<q-btn
outline
label="Cancel"
type="reset"
color="warning"
class="q-ml-sm"
:to="{ name: 'formList' }"
<q-btn
outline
label="Cancel"
type="reset"
color="warning"
class="q-ml-sm"
:to="{ name: 'formList' }"
/>
</div>
</q-form>
<div v-else-if="loading">
<q-spinner-dots
color="primary"
size="40px"
<q-spinner-dots
color="primary"
size="40px"
/>
Loading form details...
</div>
<div
v-else
class="text-negative"
<div
v-else
class="text-negative"
>
Failed to load form details.
</div>
@ -157,7 +157,7 @@
<script setup>
import { ref, onMounted } from 'vue';
import axios from 'axios';
import axios from 'boot/axios';
import { useQuasar } from 'quasar';
import { useRouter, useRoute } from 'vue-router';
@ -177,21 +177,21 @@ const loading = ref(true);
const fieldTypes = ref(['text', 'number', 'date', 'textarea', 'boolean']);
const submitting = ref(false);
async function fetchForm()
async function fetchForm()
{
loading.value = true;
try
try
{
const response = await axios.get(`/api/forms/${props.id}`);
// Ensure categories and fields exist, even if empty
response.data.categories = response.data.categories || [];
response.data.categories.forEach(cat =>
response.data.categories.forEach(cat =>
{
cat.fields = cat.fields || [];
});
form.value = response.data;
}
catch (error)
}
catch (error)
{
console.error('Error fetching form details:', error);
$q.notify({
@ -201,8 +201,8 @@ async function fetchForm()
icon: 'report_problem'
});
form.value = null; // Indicate failure
}
finally
}
finally
{
loading.value = false;
}
@ -210,38 +210,38 @@ async function fetchForm()
onMounted(fetchForm);
function addCategory()
function addCategory()
{
if (!form.value.categories)
if (!form.value.categories)
{
form.value.categories = [];
}
form.value.categories.push({ name: `Category ${form.value.categories.length + 1}`, fields: [{ label: '', type: 'text', description: '' }] });
}
function removeCategory(index)
function removeCategory(index)
{
form.value.categories.splice(index, 1);
}
function addField(catIndex)
function addField(catIndex)
{
if (!form.value.categories[catIndex].fields)
if (!form.value.categories[catIndex].fields)
{
form.value.categories[catIndex].fields = [];
}
form.value.categories[catIndex].fields.push({ label: '', type: 'text', description: '' });
}
function removeField(catIndex, fieldIndex)
function removeField(catIndex, fieldIndex)
{
form.value.categories[catIndex].fields.splice(fieldIndex, 1);
}
async function updateForm()
async function updateForm()
{
submitting.value = true;
try
try
{
// Prepare payload, potentially removing temporary IDs if any were added client-side
const payload = JSON.parse(JSON.stringify(form.value));
@ -256,8 +256,8 @@ async function updateForm()
icon: 'check_circle'
});
router.push({ name: 'formList' }); // Or maybe back to the form details/responses page
}
catch (error)
}
catch (error)
{
console.error('Error updating form:', error);
const message = error.response?.data?.error || 'Failed to update form. Please check the details and try again.';
@ -267,8 +267,8 @@ async function updateForm()
message: message,
icon: 'report_problem'
});
}
finally
}
finally
{
submitting.value = false;
}

View file

@ -1,46 +1,46 @@
<template>
<q-page padding>
<q-inner-loading :showing="loading">
<q-spinner-gears
size="50px"
color="primary"
<q-spinner-gears
size="50px"
color="primary"
/>
</q-inner-loading>
<div v-if="!loading && form">
<div class="text-h4 q-mb-xs">
{{ form.title }}
<div class="text-h4 q-mb-xs">
{{ form.title }}
</div>
<div class="text-subtitle1 text-grey q-mb-lg">
{{ form.description }}
<div class="text-subtitle1 text-grey q-mb-lg">
{{ form.description }}
</div>
<q-form
@submit.prevent="submitResponse"
class="q-gutter-md"
>
<div
v-for="category in form.categories"
:key="category.id"
class="q-mb-lg"
<q-form
@submit.prevent="submitResponse"
class="q-gutter-md"
>
<div
v-for="category in form.categories"
:key="category.id"
class="q-mb-lg"
>
<div class="text-h6 q-mb-sm">
{{ category.name }}
<div class="text-h6 q-mb-sm">
{{ category.name }}
</div>
<div
v-for="field in category.fields"
:key="field.id"
class="q-mb-md"
<div
v-for="field in category.fields"
:key="field.id"
class="q-mb-md"
>
<q-item-label class="q-mb-xs">
{{ field.label }}
<q-item-label class="q-mb-xs">
{{ field.label }}
</q-item-label>
<q-item-label
caption
v-if="field.description"
class="q-mb-xs text-grey-7"
>
{{ field.description }}
<q-item-label
caption
v-if="field.description"
class="q-mb-xs text-grey-7"
>
{{ field.description }}
</q-item-label>
<q-input
v-if="field.type === 'text'"
@ -85,38 +85,38 @@
<q-separator class="q-my-lg" />
<div>
<q-btn
outline
label="Submit Response"
type="submit"
color="primary"
:loading="submitting"
<q-btn
outline
label="Submit Response"
type="submit"
color="primary"
:loading="submitting"
/>
<q-btn
outline
label="Cancel"
type="reset"
color="default"
class="q-ml-sm"
:to="{ name: 'formList' }"
<q-btn
outline
label="Cancel"
type="reset"
color="default"
class="q-ml-sm"
:to="{ name: 'formList' }"
/>
</div>
</div>
</q-form>
</div>
<q-banner
v-else-if="!loading && !form"
class="bg-negative text-white"
<q-banner
v-else-if="!loading && !form"
class="bg-negative text-white"
>
<template #avatar>
<q-icon name="error" />
</template>
Form not found or could not be loaded.
<template #action>
<q-btn
flat
color="white"
label="Back to Forms"
:to="{ name: 'formList' }"
<q-btn
flat
color="white"
label="Back to Forms"
:to="{ name: 'formList' }"
/>
</template>
</q-banner>
@ -125,7 +125,7 @@
<script setup>
import { ref, onMounted, reactive } from 'vue';
import axios from 'axios';
import axios from 'boot/axios';
import { useQuasar } from 'quasar';
import { useRouter, useRoute } from 'vue-router';
@ -144,24 +144,24 @@ const responses = reactive({}); // Use reactive for dynamic properties
const loading = ref(true);
const submitting = ref(false);
async function fetchFormDetails()
async function fetchFormDetails()
{
loading.value = true;
form.value = null; // Reset form data
try
try
{
const response = await axios.get(`/api/forms/${props.id}`);
form.value = response.data;
// Initialize responses object based on fields
form.value.categories.forEach(cat =>
form.value.categories.forEach(cat =>
{
cat.fields.forEach(field =>
cat.fields.forEach(field =>
{
responses[field.id] = null; // Initialize all fields to null or default
});
});
}
catch (error)
}
catch (error)
{
console.error(`Error fetching form ${props.id}:`, error);
$q.notify({
@ -170,17 +170,17 @@ async function fetchFormDetails()
message: 'Failed to load form details.',
icon: 'report_problem'
});
}
finally
}
finally
{
loading.value = false;
}
}
async function submitResponse()
async function submitResponse()
{
submitting.value = true;
try
try
{
// Basic check if any response is provided (optional)
// const hasResponse = Object.values(responses).some(val => val !== null && val !== '');
@ -201,8 +201,8 @@ async function submitResponse()
// Or clear the form:
// Object.keys(responses).forEach(key => { responses[key] = null; });
}
catch (error)
}
catch (error)
{
console.error('Error submitting response:', error);
const message = error.response?.data?.error || 'Failed to submit response.';
@ -212,8 +212,8 @@ async function submitResponse()
message: message,
icon: 'report_problem'
});
}
finally
}
finally
{
submitting.value = false;
}

View file

@ -1,117 +1,117 @@
<template>
<q-page padding>
<div class="q-mb-md row justify-between items-center">
<div class="text-h4">
Forms
<div class="text-h4">
Forms
</div>
<q-btn
outline
label="Create New Form"
color="primary"
:to="{ name: 'formCreate' }"
<q-btn
outline
label="Create New Form"
color="primary"
:to="{ name: 'formCreate' }"
/>
</div>
<q-list
bordered
separator
v-if="forms.length > 0"
<q-list
bordered
separator
v-if="forms.length > 0"
>
<q-item
v-for="form in forms"
:key="form.id"
<q-item
v-for="form in forms"
:key="form.id"
>
<q-item-section>
<q-item-label>{{ form.title }}</q-item-label>
<q-item-label caption>
{{ form.description || 'No description' }}
<q-item-label caption>
{{ form.description || 'No description' }}
</q-item-label>
<q-item-label caption>
Created: {{ formatDate(form.createdAt) }}
<q-item-label caption>
Created: {{ formatDate(form.createdAt) }}
</q-item-label>
</q-item-section>
<q-item-section side>
<div class="q-gutter-sm">
<q-btn
flat
round
dense
icon="edit_note"
color="info"
:to="{ name: 'formFill', params: { id: form.id } }"
title="Fill Form"
<q-btn
flat
round
dense
icon="edit_note"
color="info"
:to="{ name: 'formFill', params: { id: form.id } }"
title="Fill Form"
/>
<q-btn
flat
round
dense
icon="visibility"
color="secondary"
:to="{ name: 'formResponses', params: { id: form.id } }"
title="View Responses"
<q-btn
flat
round
dense
icon="visibility"
color="secondary"
:to="{ name: 'formResponses', params: { id: form.id } }"
title="View Responses"
/>
<q-btn
flat
round
dense
icon="edit"
color="warning"
:to="{ name: 'formEdit', params: { id: form.id } }"
title="Edit Form"
<q-btn
flat
round
dense
icon="edit"
color="warning"
:to="{ name: 'formEdit', params: { id: form.id } }"
title="Edit Form"
/>
<q-btn
flat
round
dense
icon="delete"
color="negative"
@click.stop="confirmDeleteForm(form.id)"
title="Delete Form"
<q-btn
flat
round
dense
icon="delete"
color="negative"
@click.stop="confirmDeleteForm(form.id)"
title="Delete Form"
/>
</div>
</q-item-section>
</q-item>
</q-list>
<q-banner
v-else
class="bg-info text-white"
<q-banner
v-else
class="bg-info text-white"
>
<template #avatar>
<q-icon
name="info"
color="white"
<q-icon
name="info"
color="white"
/>
</template>
No forms created yet. Click the button above to create your first form.
</q-banner>
<q-inner-loading :showing="loading">
<q-spinner-gears
size="50px"
color="primary"
<q-spinner-gears
size="50px"
color="primary"
/>
</q-inner-loading>
</q-inner-loading>
</q-page>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import axios from 'axios';
import axios from 'boot/axios';
import { useQuasar } from 'quasar';
const $q = useQuasar();
const forms = ref([]);
const loading = ref(false);
async function fetchForms()
async function fetchForms()
{
loading.value = true;
try
try
{
const response = await axios.get('/api/forms');
forms.value = response.data;
}
catch (error)
}
catch (error)
{
console.error('Error fetching forms:', error);
$q.notify({
@ -120,15 +120,15 @@ async function fetchForms()
message: 'Failed to load forms. Please try again later.',
icon: 'report_problem'
});
}
finally
}
finally
{
loading.value = false;
}
}
// Add function to handle delete confirmation
function confirmDeleteForm(id)
function confirmDeleteForm(id)
{
$q.dialog({
title: 'Confirm Delete',
@ -144,16 +144,16 @@ function confirmDeleteForm(id)
label: 'Cancel',
flat: true
}
}).onOk(() =>
}).onOk(() =>
{
deleteForm(id);
});
}
// Add function to call the delete API
async function deleteForm(id)
async function deleteForm(id)
{
try
try
{
await axios.delete(`/api/forms/${id}`);
forms.value = forms.value.filter(form => form.id !== id);
@ -163,8 +163,8 @@ async function deleteForm(id)
message: 'Form deleted successfully.',
icon: 'check_circle'
});
}
catch (error)
}
catch (error)
{
console.error(`Error deleting form ${id}:`, error);
const errorMessage = error.response?.data?.error || 'Failed to delete form. Please try again.';
@ -178,7 +178,7 @@ async function deleteForm(id)
}
// Add function to format date
function formatDate(date)
function formatDate(date)
{
return new Date(date).toLocaleString();
}

View file

@ -99,7 +99,7 @@
<script setup>
import { ref, onMounted, computed } from 'vue';
import axios from 'axios';
import axios from 'boot/axios';
import { useQuasar } from 'quasar';
const componentProps = defineProps({

View file

@ -48,7 +48,7 @@
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { startAuthentication } from '@simplewebauthn/browser';
import axios from 'axios';
import axios from 'boot/axios';
import { useAuthStore } from 'stores/auth'; // Import the auth store
const username = ref('');
@ -65,7 +65,7 @@ async function handleLogin()
try
{
// 1. Get options from server
const optionsRes = await axios.post('/auth/generate-authentication-options', {
const optionsRes = await axios.post('/api/auth/generate-authentication-options', {
username: username.value || undefined, // Send username if provided
});
const options = optionsRes.data;
@ -74,7 +74,7 @@ async function handleLogin()
const authResp = await startAuthentication(options);
// 3. Send response to server for verification
const verificationRes = await axios.post('/auth/verify-authentication', {
const verificationRes = await axios.post('/api/auth/verify-authentication', {
authenticationResponse: authResp,
});

View file

@ -69,7 +69,7 @@
<q-item-label
class="q-mt-sm markdown-content"
>
<div v-html="parseMarkdown(summary.content)" />
<div v-html="parseMarkdown(summary.summaryText)" />
</q-item-label>
</q-item-section>
</q-item>
@ -102,7 +102,7 @@
<script setup>
import { ref, onMounted, computed } from 'vue';
import { date, useQuasar } from 'quasar'; // Import useQuasar
import axios from 'axios';
import axios from 'boot/axios';
import { marked } from 'marked';
const $q = useQuasar(); // Initialize Quasar plugin usage

View file

@ -1,8 +1,8 @@
<template>
<q-page padding>
<div class="q-mb-md row justify-between items-center">
<div class="text-h4">
Passkey Management
<div class="text-h4">
Passkey Management
</div>
<div>
<q-btn
@ -29,23 +29,23 @@
<!-- Passkey List Section -->
<q-card-section>
<h5>Your Registered Passkeys</h5>
<q-list
bordered
separator
v-if="passkeys.length > 0 && !fetchLoading"
<q-list
bordered
separator
v-if="passkeys.length > 0 && !fetchLoading"
>
<q-item v-if="registerSuccessMessage || registerErrorMessage">
<div
v-if="registerSuccessMessage"
class="text-positive q-mt-md"
>
{{ registerSuccessMessage }}
<div
v-if="registerSuccessMessage"
class="text-positive q-mt-md"
>
{{ registerSuccessMessage }}
</div>
<div
v-if="registerErrorMessage"
class="text-negative q-mt-md"
>
{{ registerErrorMessage }}
<div
v-if="registerErrorMessage"
class="text-negative q-mt-md"
>
{{ registerErrorMessage }}
</div>
</q-item>
<q-item
@ -55,19 +55,19 @@
>
<q-item-section>
<q-item-label>Passkey ID: {{ passkey.credentialID }} </q-item-label>
<q-item-label
caption
v-if="identifiedPasskeyId === passkey.credentialID"
<q-item-label
caption
v-if="identifiedPasskeyId === passkey.credentialID"
>
Verified just now!
</q-item-label>
<!-- <q-item-label caption>Registered: {{ new Date(passkey.createdAt).toLocaleDateString() }}</q-item-label> -->
</q-item-section>
<q-item-section
side
class="row no-wrap items-center"
>
<q-item-section
side
class="row no-wrap items-center"
>
<!-- Delete Button -->
<q-btn
flat
@ -82,51 +82,51 @@
</q-item-section>
</q-item>
</q-list>
<div
v-else-if="fetchLoading"
class="q-mt-md"
>
Loading passkeys...
<div
v-else-if="fetchLoading"
class="q-mt-md"
>
Loading passkeys...
</div>
<div
v-else
class="q-mt-md"
>
You have no passkeys registered yet.
<div
v-else
class="q-mt-md"
>
You have no passkeys registered yet.
</div>
<div
v-if="fetchErrorMessage"
class="text-negative q-mt-md"
>
{{ fetchErrorMessage }}
<div
v-if="fetchErrorMessage"
class="text-negative q-mt-md"
>
{{ fetchErrorMessage }}
</div>
<div
v-if="deleteSuccessMessage"
class="text-positive q-mt-md"
>
{{ deleteSuccessMessage }}
<div
v-if="deleteSuccessMessage"
class="text-positive q-mt-md"
>
{{ deleteSuccessMessage }}
</div>
<div
v-if="deleteErrorMessage"
class="text-negative q-mt-md"
>
{{ deleteErrorMessage }}
<div
v-if="deleteErrorMessage"
class="text-negative q-mt-md"
>
{{ deleteErrorMessage }}
</div>
<div
v-if="identifyErrorMessage"
class="text-negative q-mt-md"
>
{{ identifyErrorMessage }}
</div>
</q-card-section>
<div
v-if="identifyErrorMessage"
class="text-negative q-mt-md"
>
{{ identifyErrorMessage }}
</div>
</q-card-section>
</q-page>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import { startRegistration, startAuthentication } from '@simplewebauthn/browser'; // Import startAuthentication
import axios from 'axios';
import axios from 'boot/axios';
import { useAuthStore } from 'stores/auth';
const registerLoading = ref(false);
@ -149,7 +149,7 @@ const isLoggedIn = computed(() => authStore.isAuthenticated);
const username = computed(() => authStore.user?.username);
// Fetch existing passkeys
async function fetchPasskeys()
async function fetchPasskeys()
{
if (!isLoggedIn.value) return;
fetchLoading.value = true;
@ -158,49 +158,49 @@ async function fetchPasskeys()
deleteErrorMessage.value = '';
identifyErrorMessage.value = ''; // Clear identify message
identifiedPasskeyId.value = null; // Clear identified key
try
try
{
const response = await axios.get('/auth/passkeys');
const response = await axios.get('/api/auth/passkeys');
passkeys.value = response.data || [];
}
catch (error)
}
catch (error)
{
console.error('Error fetching passkeys:', error);
fetchErrorMessage.value = error.response?.data?.error || 'Failed to load passkeys.';
passkeys.value = []; // Clear passkeys on error
}
finally
}
finally
{
fetchLoading.value = false;
}
}
// Check auth status and fetch passkeys on component mount
onMounted(async() =>
onMounted(async() =>
{
let initialAuthError = '';
if (!authStore.isAuthenticated)
if (!authStore.isAuthenticated)
{
await authStore.checkAuthStatus();
if (authStore.error)
if (authStore.error)
{
initialAuthError = `Authentication error: ${authStore.error}`;
}
}
if (!isLoggedIn.value)
if (!isLoggedIn.value)
{
// Use register error message ref for consistency if login is required first
registerErrorMessage.value = initialAuthError || 'You must be logged in to manage passkeys.';
}
else
}
else
{
fetchPasskeys(); // Fetch passkeys if logged in
}
});
async function handleRegister()
async function handleRegister()
{
if (!isLoggedIn.value || !username.value)
if (!isLoggedIn.value || !username.value)
{
registerErrorMessage.value = 'User not authenticated.';
return;
@ -213,10 +213,10 @@ async function handleRegister()
identifyErrorMessage.value = '';
identifiedPasskeyId.value = null;
try
try
{
// 1. Get options from server
const optionsRes = await axios.post('/auth/generate-registration-options', {
const optionsRes = await axios.post('/api/auth/generate-registration-options', {
username: username.value, // Use username from store
});
const options = optionsRes.data;
@ -225,43 +225,43 @@ async function handleRegister()
const regResp = await startRegistration(options);
// 3. Send response to server for verification
const verificationRes = await axios.post('/auth/verify-registration', {
const verificationRes = await axios.post('/api/auth/verify-registration', {
registrationResponse: regResp,
});
if (verificationRes.data.verified)
if (verificationRes.data.verified)
{
registerSuccessMessage.value = 'New passkey registered successfully!';
fetchPasskeys(); // Refresh the list of passkeys
}
else
}
else
{
registerErrorMessage.value = 'Passkey verification failed.';
}
}
catch (error)
}
catch (error)
{
console.error('Registration error:', error);
const message = error.response?.data?.error || error.message || 'An unknown error occurred during registration.';
// Handle specific simplewebauthn errors
if (error.name === 'InvalidStateError')
if (error.name === 'InvalidStateError')
{
registerErrorMessage.value = 'Authenticator may already be registered.';
}
else if (error.name === 'NotAllowedError')
}
else if (error.name === 'NotAllowedError')
{
registerErrorMessage.value = 'Registration ceremony was cancelled or timed out.';
}
else if (error.response?.status === 409)
}
else if (error.response?.status === 409)
{
registerErrorMessage.value = 'This passkey seems to be registered already.';
}
else
}
else
{
registerErrorMessage.value = `Registration failed: ${message}`;
}
}
finally
}
finally
{
registerLoading.value = false;
}
@ -269,7 +269,7 @@ async function handleRegister()
// Handle deleting a passkey
async function handleDelete(credentialID)
async function handleDelete(credentialID)
{
if (!credentialID) return;
@ -286,27 +286,27 @@ async function handleDelete(credentialID)
identifyErrorMessage.value = '';
identifiedPasskeyId.value = null;
try
try
{
await axios.delete(`/auth/passkeys/${credentialID}`);
await axios.delete(`/api/auth/passkeys/${credentialID}`);
deleteSuccessMessage.value = 'Passkey deleted successfully.';
fetchPasskeys(); // Refresh the list
}
catch (error)
}
catch (error)
{
console.error('Error deleting passkey:', error);
deleteErrorMessage.value = error.response?.data?.error || 'Failed to delete passkey.';
}
finally
}
finally
{
deleteLoading.value = null; // Clear loading state
}
}
// Handle identifying a passkey
async function handleIdentify()
async function handleIdentify()
{
if (!isLoggedIn.value)
if (!isLoggedIn.value)
{
identifyErrorMessage.value = 'You must be logged in.';
return;
@ -321,11 +321,11 @@ async function handleIdentify()
deleteSuccessMessage.value = '';
deleteErrorMessage.value = '';
try
try
{
// 1. Get authentication options from the server
// We don't need to send username as the server should use the session
const optionsRes = await axios.post('/auth/generate-authentication-options', {}); // Send empty body
const optionsRes = await axios.post('/api/auth/generate-authentication-options', {}); // Send empty body
const options = optionsRes.data;
// Optionally filter options to only allow the specific key if needed, but usually not necessary for identification
@ -339,30 +339,30 @@ async function handleIdentify()
console.log('Identified Passkey ID:', identifiedPasskeyId.value);
// Optional: Add a small delay before clearing the highlight
setTimeout(() =>
setTimeout(() =>
{
// Only clear if it's still the same identified key
if (identifiedPasskeyId.value === authResp.id)
if (identifiedPasskeyId.value === authResp.id)
{
identifiedPasskeyId.value = null;
}
}, 5000); // Clear highlight after 5 seconds
}
catch (error)
}
catch (error)
{
console.error('Identification error:', error);
identifiedPasskeyId.value = null;
if (error.name === 'NotAllowedError')
if (error.name === 'NotAllowedError')
{
identifyErrorMessage.value = 'Identification ceremony was cancelled or timed out.';
}
else
}
else
{
identifyErrorMessage.value = error.response?.data?.error || error.message || 'Failed to identify passkey.';
}
}
finally
}
finally
{
identifyLoading.value = null; // Clear loading state
}

View file

@ -3,8 +3,8 @@
<q-card style="width: 400px; max-width: 90vw;">
<q-card-section>
<!-- Update title based on login status from store -->
<div class="text-h6">
{{ isLoggedIn ? 'Register New Passkey' : 'Register Passkey' }}
<div class="text-h6">
{{ isLoggedIn ? 'Register New Passkey' : 'Register Passkey' }}
</div>
</q-card-section>
@ -29,27 +29,27 @@
:loading="loading"
:disable="loading || (!username && !isLoggedIn)"
/>
<div
v-if="successMessage"
class="text-positive q-mt-md"
>
{{ successMessage }}
<div
v-if="successMessage"
class="text-positive q-mt-md"
>
{{ successMessage }}
</div>
<div
v-if="errorMessage"
class="text-negative q-mt-md"
>
{{ errorMessage }}
<div
v-if="errorMessage"
class="text-negative q-mt-md"
>
{{ errorMessage }}
</div>
</q-card-section>
<q-card-actions align="center">
<!-- Hide login link if already logged in based on store state -->
<q-btn
v-if="!isLoggedIn"
flat
label="Already have an account? Login"
to="/login"
<q-btn
v-if="!isLoggedIn"
flat
label="Already have an account? Login"
to="/login"
/>
</q-card-actions>
</q-card>
@ -60,7 +60,7 @@
import { ref, onMounted, computed } from 'vue'; // Import computed
import { useRouter } from 'vue-router';
import { startRegistration } from '@simplewebauthn/browser';
import axios from 'axios';
import axios from 'boot/axios';
import { useAuthStore } from 'stores/auth'; // Import the auth store
const loading = ref(false);
@ -75,31 +75,31 @@ const isLoggedIn = computed(() => authStore.isAuthenticated);
const username = ref(''); // Local ref for username input
// Check auth status on component mount using the store action
onMounted(async() =>
onMounted(async() =>
{
if (!authStore.isAuthenticated)
if (!authStore.isAuthenticated)
{
await authStore.checkAuthStatus();
if (authStore.error)
if (authStore.error)
{
errorMessage.value = authStore.error;
}
}
if (!isLoggedIn.value)
if (!isLoggedIn.value)
{
username.value = ''; // Clear username if not logged in
}
else
}
else
{
username.value = authStore.user?.username || ''; // Use username from store if logged in
}
});
async function handleRegister()
async function handleRegister()
{
const currentUsername = isLoggedIn.value ? authStore.user?.username : username.value;
if (!currentUsername)
if (!currentUsername)
{
errorMessage.value = 'Username is missing.';
return;
@ -108,10 +108,10 @@ async function handleRegister()
errorMessage.value = '';
successMessage.value = '';
try
try
{
// 1. Get options from server
const optionsRes = await axios.post('/auth/generate-registration-options', {
const optionsRes = await axios.post('/api/auth/generate-registration-options', {
username: currentUsername, // Use username from store
});
const options = optionsRes.data;
@ -120,58 +120,58 @@ async function handleRegister()
const regResp = await startRegistration(options);
// 3. Send response to server for verification
const verificationRes = await axios.post('/auth/verify-registration', {
const verificationRes = await axios.post('/api/auth/verify-registration', {
registrationResponse: regResp,
});
if (verificationRes.data.verified)
if (verificationRes.data.verified)
{
// Adjust success message based on login state
successMessage.value = isLoggedIn.value
? 'New passkey registered successfully!'
: 'Registration successful! Redirecting to login...';
if (!isLoggedIn.value)
if (!isLoggedIn.value)
{
// Redirect to login page only if they weren't logged in
setTimeout(() =>
setTimeout(() =>
{
router.push('/login');
}, 2000);
}
else
}
else
{
// Maybe redirect to a profile page or dashboard if already logged in
// setTimeout(() => { router.push('/dashboard'); }, 2000);
}
}
else
}
else
{
errorMessage.value = 'Registration failed.';
}
}
catch (error)
}
catch (error)
{
console.error('Registration error:', error);
const message = error.response?.data?.error || error.message || 'An unknown error occurred during registration.';
// Handle specific simplewebauthn errors
if (error.name === 'InvalidStateError')
if (error.name === 'InvalidStateError')
{
errorMessage.value = 'Authenticator already registered. Try logging in instead.';
}
else if (error.name === 'NotAllowedError')
}
else if (error.name === 'NotAllowedError')
{
errorMessage.value = 'Registration ceremony was cancelled or timed out.';
}
else if (error.response?.status === 409)
}
else if (error.response?.status === 409)
{
errorMessage.value = 'This passkey seems to be registered already.';
}
else
}
else
{
errorMessage.value = `Registration failed: ${message}`;
}
}
finally
}
finally
{
loading.value = false;
}

View file

@ -1,20 +1,20 @@
<template>
<q-page padding>
<div
class="q-gutter-md"
style="max-width: 800px; margin: auto;"
<div
class="q-gutter-md"
style="max-width: 800px; margin: auto;"
>
<h5 class="q-mt-none q-mb-md">
Settings
<h5 class="q-mt-none q-mb-md">
Settings
</h5>
<q-card
flat
bordered
<q-card
flat
bordered
>
<q-card-section>
<div class="text-h6">
Mantis Summary Prompt
<div class="text-h6">
Mantis Summary Prompt
</div>
<div class="text-caption text-grey q-mb-sm">
Edit the prompt used to generate Mantis summaries. Use $DATE and $MANTIS_TICKETS as placeholders.
@ -40,13 +40,13 @@
</q-card-actions>
</q-card>
<q-card
flat
bordered
<q-card
flat
bordered
>
<q-card-section>
<div class="text-h6">
Email Summary Prompt
<div class="text-h6">
Email Summary Prompt
</div>
<div class="text-caption text-grey q-mb-sm">
Edit the prompt used to generate Email summaries. Use $EMAIL_DATA as a placeholder for the JSON email array.
@ -70,7 +70,7 @@
:disable="!emailPrompt || loadingEmailPrompt"
/>
</q-card-actions>
</q-card>
</q-card>
</div>
</q-page>
</template>
@ -78,7 +78,7 @@
<script setup>
import { ref, onMounted } from 'vue';
import { useQuasar } from 'quasar';
import axios from 'axios';
import axios from 'boot/axios';
const $q = useQuasar();
@ -86,15 +86,15 @@ const mantisPrompt = ref('');
const loadingPrompt = ref(false);
const savingPrompt = ref(false);
const fetchMantisPrompt = async() =>
const fetchMantisPrompt = async() =>
{
loadingPrompt.value = true;
try
try
{
const response = await axios.get('/api/settings/mantisPrompt');
mantisPrompt.value = response.data.value || ''; // Handle case where setting might not exist yet
}
catch (error)
}
catch (error)
{
console.error('Error fetching Mantis prompt:', error);
$q.notify({
@ -102,17 +102,17 @@ const fetchMantisPrompt = async() =>
message: 'Failed to load Mantis prompt setting.',
icon: 'report_problem'
});
}
finally
}
finally
{
loadingPrompt.value = false;
}
};
const saveMantisPrompt = async() =>
const saveMantisPrompt = async() =>
{
savingPrompt.value = true;
try
try
{
await axios.put('/api/settings/mantisPrompt', { value: mantisPrompt.value });
$q.notify({
@ -120,8 +120,8 @@ const saveMantisPrompt = async() =>
message: 'Mantis prompt updated successfully.',
icon: 'check_circle'
});
}
catch (error)
}
catch (error)
{
console.error('Error saving Mantis prompt:', error);
$q.notify({
@ -129,8 +129,8 @@ const saveMantisPrompt = async() =>
message: 'Failed to save Mantis prompt setting.',
icon: 'report_problem'
});
}
finally
}
finally
{
savingPrompt.value = false;
}
@ -140,15 +140,15 @@ const emailPrompt = ref('');
const loadingEmailPrompt = ref(false);
const savingEmailPrompt = ref(false);
const fetchEmailPrompt = async() =>
const fetchEmailPrompt = async() =>
{
loadingEmailPrompt.value = true;
try
try
{
const response = await axios.get('/api/settings/emailPrompt');
emailPrompt.value = response.data.value || ''; // Handle case where setting might not exist yet
}
catch (error)
}
catch (error)
{
console.error('Error fetching Email prompt:', error);
$q.notify({
@ -156,17 +156,17 @@ const fetchEmailPrompt = async() =>
message: 'Failed to load Email prompt setting.',
icon: 'report_problem'
});
}
finally
}
finally
{
loadingEmailPrompt.value = false;
}
};
const saveEmailPrompt = async() =>
const saveEmailPrompt = async() =>
{
savingEmailPrompt.value = true;
try
try
{
await axios.put('/api/settings/emailPrompt', { value: emailPrompt.value });
$q.notify({
@ -174,8 +174,8 @@ const saveEmailPrompt = async() =>
message: 'Email prompt updated successfully.',
icon: 'check_circle'
});
}
catch (error)
}
catch (error)
{
console.error('Error saving Email prompt:', error);
$q.notify({
@ -183,14 +183,14 @@ const saveEmailPrompt = async() =>
message: 'Failed to save Email prompt setting.',
icon: 'report_problem'
});
}
finally
}
finally
{
savingEmailPrompt.value = false;
}
};
onMounted(() =>
onMounted(() =>
{
fetchMantisPrompt();
fetchEmailPrompt();

View file

@ -1,5 +1,5 @@
import { defineStore } from 'pinia';
import axios from 'axios';
import axios from 'boot/axios';
export const useAuthStore = defineStore('auth', {
state: () => ({
@ -15,8 +15,10 @@ export const useAuthStore = defineStore('auth', {
this.error = null;
try
{
const res = await axios.get('/auth/check-auth');
if (res.data.isAuthenticated)
const res = await axios.get('/api/auth/status', {
withCredentials: true, // Ensure cookies are sent with the request
});
if (res.data.status === 'authenticated')
{
this.isAuthenticated = true;
this.user = res.data.user;

View file

@ -1,8 +1,8 @@
import { defineStore } from 'pinia';
import { ref, computed, watch } from 'vue'; // Import watch
import axios from 'axios';
import axios from 'boot/axios';
export const useChatStore = defineStore('chat', () =>
export const useChatStore = defineStore('chat', () =>
{
const isVisible = ref(false);
const currentThreadId = ref(null);
@ -19,13 +19,13 @@ export const useChatStore = defineStore('chat', () =>
// --- Actions ---
// New action to create a thread if it doesn't exist
async function createThreadIfNotExists()
async function createThreadIfNotExists()
{
if (currentThreadId.value) return; // Already have a thread
isLoading.value = true;
error.value = null;
try
try
{
// Call the endpoint without content to just create the thread
const response = await axios.post('/api/chat/threads', {});
@ -34,50 +34,50 @@ export const useChatStore = defineStore('chat', () =>
console.log('Created new chat thread:', currentThreadId.value);
// Start polling now that we have a thread ID
startPolling();
}
catch (err)
}
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
}
finally
{
isLoading.value = false;
}
}
function toggleChat()
function toggleChat()
{
isVisible.value = !isVisible.value;
if (isVisible.value)
if (isVisible.value)
{
if (!currentThreadId.value)
if (!currentThreadId.value)
{
// If opening and no thread exists, create one
createThreadIfNotExists();
}
else
}
else
{
// If opening and thread exists, fetch messages if empty and start polling
if (messages.value.length === 0)
if (messages.value.length === 0)
{
fetchMessages();
}
startPolling();
}
}
else
}
else
{
// If closing, stop polling
stopPolling();
}
}
async function fetchMessages()
async function fetchMessages()
{
if (!currentThreadId.value)
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.
@ -86,7 +86,7 @@ export const useChatStore = defineStore('chat', () =>
// 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
try
{
const response = await axios.get(`/api/chat/threads/${currentThreadId.value}/messages`);
const newMessages = response.data.map(msg => ({
@ -97,28 +97,28 @@ export const useChatStore = defineStore('chat', () =>
})).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))
if (JSON.stringify(messages.value) !== JSON.stringify(newMessages))
{
messages.value = newMessages;
}
}
catch (err)
}
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
}
finally
{
// isLoading.value = false;
}
}
// Function to start polling
function startPolling()
function startPolling()
{
if (pollingIntervalId.value) return; // Already polling
if (!currentThreadId.value) return; // No thread to poll for
@ -128,9 +128,9 @@ export const useChatStore = defineStore('chat', () =>
}
// Function to stop polling
function stopPolling()
function stopPolling()
{
if (pollingIntervalId.value)
if (pollingIntervalId.value)
{
console.log('Stopping chat polling.');
clearInterval(pollingIntervalId.value);
@ -139,10 +139,10 @@ export const useChatStore = defineStore('chat', () =>
}
async function sendMessage(content)
async function sendMessage(content)
{
if (!content.trim()) return;
if (!currentThreadId.value)
if (!currentThreadId.value)
{
error.value = 'Cannot send message: No active chat thread.';
console.error('Attempted to send message without a thread ID.');
@ -165,7 +165,7 @@ export const useChatStore = defineStore('chat', () =>
isLoading.value = true; // Indicate activity
error.value = null;
try
try
{
const payload = { content: userMessage.content };
// Always post to the existing thread once it's created
@ -180,8 +180,8 @@ export const useChatStore = defineStore('chat', () =>
// Immediately fetch messages after sending to get the updated list
await fetchMessages();
}
catch (err)
}
catch (err)
{
console.error('Error sending message:', err);
error.value = 'Failed to send message.';
@ -190,8 +190,8 @@ export const useChatStore = defineStore('chat', () =>
// 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
}
finally
{
isLoading.value = false;
// Restart polling after sending attempt is complete
@ -200,7 +200,7 @@ export const useChatStore = defineStore('chat', () =>
}
// Call this when the user logs out or the app closes if you want to clear state
function resetChat()
function resetChat()
{
stopPolling(); // Ensure polling stops on reset
isVisible.value = false;