mirror of
https://github.com/Litlyx/litlyx
synced 2025-12-10 15:58:38 +01:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa5a37ece2 | ||
|
|
db32afe741 | ||
|
|
e813b3246d | ||
|
|
86011c38ce | ||
|
|
fd5eca29cc | ||
|
|
a591b43600 | ||
|
|
cebb45484c | ||
|
|
e4e2c2a42a | ||
|
|
dfa1407102 | ||
|
|
e6adbf9c7b | ||
|
|
c3904ebd55 | ||
|
|
4c46a36c75 | ||
|
|
c253846b86 | ||
|
|
e7c2dbf237 | ||
|
|
525a371a6e | ||
|
|
6a9a698b7a |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -2,4 +2,5 @@ steps
|
|||||||
PROCESS_EVENT
|
PROCESS_EVENT
|
||||||
docker
|
docker
|
||||||
dev
|
dev
|
||||||
docker-compose.admin.yml
|
docker-compose.admin.yml
|
||||||
|
full_reload.sh
|
||||||
@@ -11,8 +11,6 @@ if (process.env.EMAIL_SERVICE) {
|
|||||||
|
|
||||||
export async function checkLimitsForEmail(projectCounts: TProjectLimit) {
|
export async function checkLimitsForEmail(projectCounts: TProjectLimit) {
|
||||||
|
|
||||||
console.log('CHECK LIMIT EMAIL');
|
|
||||||
|
|
||||||
const project_id = projectCounts.project_id;
|
const project_id = projectCounts.project_id;
|
||||||
const hasNotifyEntry = await LimitNotifyModel.findOne({ project_id });
|
const hasNotifyEntry = await LimitNotifyModel.findOne({ project_id });
|
||||||
if (!hasNotifyEntry) {
|
if (!hasNotifyEntry) {
|
||||||
@@ -20,7 +18,6 @@ export async function checkLimitsForEmail(projectCounts: TProjectLimit) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ((projectCounts.visits + projectCounts.events) >= (projectCounts.limit)) {
|
if ((projectCounts.visits + projectCounts.events) >= (projectCounts.limit)) {
|
||||||
console.log('LIMIT 3');
|
|
||||||
|
|
||||||
const notify = await LimitNotifyModel.findOne({ project_id });
|
const notify = await LimitNotifyModel.findOne({ project_id });
|
||||||
if (notify && notify.limit3 === true) return;
|
if (notify && notify.limit3 === true) return;
|
||||||
@@ -35,7 +32,6 @@ export async function checkLimitsForEmail(projectCounts: TProjectLimit) {
|
|||||||
await LimitNotifyModel.updateOne({ project_id: projectCounts.project_id }, { limit1: true, limit2: true, limit3: true });
|
await LimitNotifyModel.updateOne({ project_id: projectCounts.project_id }, { limit1: true, limit2: true, limit3: true });
|
||||||
|
|
||||||
} else if ((projectCounts.visits + projectCounts.events) >= (projectCounts.limit * 0.9)) {
|
} else if ((projectCounts.visits + projectCounts.events) >= (projectCounts.limit * 0.9)) {
|
||||||
console.log('LIMIT 2');
|
|
||||||
|
|
||||||
const notify = await LimitNotifyModel.findOne({ project_id });
|
const notify = await LimitNotifyModel.findOne({ project_id });
|
||||||
if (notify && notify.limit2 === true) return;
|
if (notify && notify.limit2 === true) return;
|
||||||
@@ -51,8 +47,6 @@ export async function checkLimitsForEmail(projectCounts: TProjectLimit) {
|
|||||||
|
|
||||||
} else if ((projectCounts.visits + projectCounts.events) >= (projectCounts.limit * 0.5)) {
|
} else if ((projectCounts.visits + projectCounts.events) >= (projectCounts.limit * 0.5)) {
|
||||||
|
|
||||||
console.log('LIMIT 1');
|
|
||||||
|
|
||||||
const notify = await LimitNotifyModel.findOne({ project_id });
|
const notify = await LimitNotifyModel.findOne({ project_id });
|
||||||
if (notify && notify.limit1 === true) return;
|
if (notify && notify.limit1 === true) return;
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ export async function startStreamLoop() {
|
|||||||
await RedisStreamService.startReadingLoop({
|
await RedisStreamService.startReadingLoop({
|
||||||
streamName: requireEnv('STREAM_NAME'),
|
streamName: requireEnv('STREAM_NAME'),
|
||||||
delay: { base: 10, empty: 5000 },
|
delay: { base: 10, empty: 5000 },
|
||||||
readBlock: 2000
|
readBlock: 2000,
|
||||||
|
consumer: 'consumer_' + process.env.NODE_APP_INSTANCE
|
||||||
}, processStreamEvent);
|
}, processStreamEvent);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
1
dashboard/.gitignore
vendored
1
dashboard/.gitignore
vendored
@@ -12,6 +12,7 @@ node_modules
|
|||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
*.log
|
*.log
|
||||||
|
winston-*.ndjson
|
||||||
|
|
||||||
# Misc
|
# Misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@@ -12,12 +12,6 @@ const { showDialog, closeDialog, dialogComponent, dialogParams, dialogStyle, dia
|
|||||||
|
|
||||||
const { visible } = usePricingDrawer();
|
const { visible } = usePricingDrawer();
|
||||||
|
|
||||||
const { data: planData } = useFetch('/api/project/plan', {
|
|
||||||
...signHeaders(),
|
|
||||||
lazy: true
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -25,10 +19,10 @@ const { data: planData } = useFetch('/api/project/plan', {
|
|||||||
<div class="w-dvw h-dvh bg-lyx-background-light relative">
|
<div class="w-dvw h-dvh bg-lyx-background-light relative">
|
||||||
|
|
||||||
<Transition name="pdrawer">
|
<Transition name="pdrawer">
|
||||||
<LazyPricingDrawer @onCloseClick="visible = false" :currentSub="planData?.premium_type || 0"
|
<LazyPricingDrawer @onCloseClick="visible = false"
|
||||||
class="bg-black fixed right-0 top-0 w-full xl:w-[60vw] xl:min-w-[65rem] h-full z-[20]" v-if=visible>
|
class="bg-black fixed right-0 top-0 w-full xl:w-[60vw] xl:min-w-[65rem] h-full z-[20]" v-if="visible">
|
||||||
</LazyPricingDrawer>
|
</LazyPricingDrawer>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
|
||||||
|
|
||||||
<div class="fixed top-4 right-8 z-[999] flex flex-col gap-2" v-if="alerts.length > 0">
|
<div class="fixed top-4 right-8 z-[999] flex flex-col gap-2" v-if="alerts.length > 0">
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ const chartData = ref<ChartData<'bar'>>({
|
|||||||
label: e.label || '?',
|
label: e.label || '?',
|
||||||
backgroundColor: [e.color],
|
backgroundColor: [e.color],
|
||||||
borderWidth: 0,
|
borderWidth: 0,
|
||||||
borderRadius: 8
|
borderRadius: 0
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export type Entry = {
|
|||||||
icon?: string,
|
icon?: string,
|
||||||
action?: () => any,
|
action?: () => any,
|
||||||
adminOnly?: boolean,
|
adminOnly?: boolean,
|
||||||
premiumOnly?:boolean,
|
premiumOnly?: boolean,
|
||||||
external?: boolean,
|
external?: boolean,
|
||||||
grow?: boolean
|
grow?: boolean
|
||||||
}
|
}
|
||||||
@@ -28,6 +28,7 @@ const route = useRoute();
|
|||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
const { isAdmin } = useUserRoles();
|
const { isAdmin } = useUserRoles();
|
||||||
|
const loggedUser = useLoggedUser()
|
||||||
|
|
||||||
const debugMode = process.dev;
|
const debugMode = process.dev;
|
||||||
|
|
||||||
@@ -101,8 +102,15 @@ function onLogout() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { projects } = useProjectsList();
|
const { projects } = useProjectsList();
|
||||||
|
const { data: guestProjects } = useGuestProjectsList()
|
||||||
const activeProject = useActiveProject();
|
const activeProject = useActiveProject();
|
||||||
|
|
||||||
|
const selectorProjects = computed(() => {
|
||||||
|
const result: TProject[] = [];
|
||||||
|
if (projects.value) result.push(...projects.value);
|
||||||
|
if (guestProjects.value) result.push(...guestProjects.value);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
const { data: maxProjects } = useFetch("/api/user/max_projects", {
|
const { data: maxProjects } = useFetch("/api/user/max_projects", {
|
||||||
headers: computed(() => {
|
headers: computed(() => {
|
||||||
@@ -117,10 +125,18 @@ watch(selected, () => {
|
|||||||
setActiveProject(selected.value._id.toString())
|
setActiveProject(selected.value._id.toString())
|
||||||
})
|
})
|
||||||
|
|
||||||
const isPremium = computed(()=>{
|
const isPremium = computed(() => {
|
||||||
return activeProject.value?.premium;
|
return activeProject.value?.premium;
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function isProjectMine(owner?: string) {
|
||||||
|
if (!owner) return false;
|
||||||
|
if (!loggedUser.value?.logged) return;
|
||||||
|
return loggedUser.value.id == owner;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pricingDrawer = usePricingDrawer();
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -131,6 +147,13 @@ const isPremium = computed(()=>{
|
|||||||
}">
|
}">
|
||||||
<div class="py-4 px-2 gap-6 flex flex-col w-full">
|
<div class="py-4 px-2 gap-6 flex flex-col w-full">
|
||||||
|
|
||||||
|
<!-- <div class="flex px-2" v-if="!isPremium">
|
||||||
|
<LyxUiButton type="primary" class="w-full text-center text-[.8rem] font-medium" @click="pricingDrawer.visible.value = true;">
|
||||||
|
Upgrade plan
|
||||||
|
</LyxUiButton>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
|
||||||
<div class="flex px-2 flex-col">
|
<div class="flex px-2 flex-col">
|
||||||
|
|
||||||
<div class="flex items-center gap-2 w-full">
|
<div class="flex items-center gap-2 w-full">
|
||||||
@@ -142,14 +165,14 @@ const isPremium = computed(()=>{
|
|||||||
base: 'hover:!bg-lyx-widget-lighter cursor-pointer',
|
base: 'hover:!bg-lyx-widget-lighter cursor-pointer',
|
||||||
active: '!bg-lyx-widget-lighter'
|
active: '!bg-lyx-widget-lighter'
|
||||||
}
|
}
|
||||||
}" class="w-full" v-if="projects" v-model="selected" :options="projects">
|
}" class="w-full" v-if="selectorProjects" v-model="selected" :options="selectorProjects">
|
||||||
|
|
||||||
<template #option="{ option, active, selected }">
|
<template #option="{ option, active, selected }">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div>
|
<div>
|
||||||
<img class="h-5 bg-black rounded-full" :src="'/logo_32.png'" alt="Litlyx logo">
|
<img class="h-5 bg-black rounded-full" :src="'/logo_32.png'" alt="Litlyx logo">
|
||||||
</div>
|
</div>
|
||||||
<div> {{ option.name }} </div>
|
<div> {{ option.name }} {{ !isProjectMine(option.owner) ? '(Guest)' : '' }}</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -158,7 +181,10 @@ const isPremium = computed(()=>{
|
|||||||
<div>
|
<div>
|
||||||
<img class="h-5 bg-black rounded-full" :src="'/logo_32.png'" alt="Litlyx logo">
|
<img class="h-5 bg-black rounded-full" :src="'/logo_32.png'" alt="Litlyx logo">
|
||||||
</div>
|
</div>
|
||||||
<div> {{ activeProject?.name || '???' }} </div>
|
<div>
|
||||||
|
{{ activeProject?.name || '-' }}
|
||||||
|
{{ !isProjectMine(activeProject?.owner?.toString()) ? '(Guest)' : '' }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</USelectMenu>
|
</USelectMenu>
|
||||||
@@ -169,6 +195,7 @@ const isPremium = computed(()=>{
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<NuxtLink to="/project_creation" v-if="projects && (projects.length < (maxProjects || 1))"
|
<NuxtLink to="/project_creation" v-if="projects && (projects.length < (maxProjects || 1))"
|
||||||
class="flex items-center text-[.8rem] gap-1 justify-end pt-2 pr-2 text-lyx-text-dark hover:text-lyx-text cursor-pointer">
|
class="flex items-center text-[.8rem] gap-1 justify-end pt-2 pr-2 text-lyx-text-dark hover:text-lyx-text cursor-pointer">
|
||||||
<div><i class="fas fa-plus"></i></div>
|
<div><i class="fas fa-plus"></i></div>
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ const props = defineProps<{ title: string, sub?: string }>();
|
|||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex flex-col grow">
|
<div class="flex flex-col grow">
|
||||||
<div class="poppins font-semibold text-[1.1rem] md:text-[1.4rem] text-text">
|
<div class="poppins font-semibold text-[1rem] md:text-[1.3rem] text-text">
|
||||||
{{ props.title }}
|
{{ props.title }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="props.sub" class="poppins text-[.8rem] md:text-[1.1rem] text-text-sub">
|
<div v-if="props.sub" class="poppins text-[.7rem] md:text-[1rem] text-text-sub">
|
||||||
{{ props.sub }}
|
{{ props.sub }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ const emits = defineEmits<{
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
||||||
<div class="flex gap-2 border-[1px] border-gray-400 p-1 md:p-2 rounded-xl">
|
<div class="flex gap-2 border-[1px] border-lyx-widget-lighter p-1 md:p-2 rounded-xl bg-lyx-widget">
|
||||||
<div @click="$emit('changeIndex', index)" v-for="(opt, index) of options"
|
<div @click="$emit('changeIndex', index)" v-for="(opt, index) of options"
|
||||||
class="hover:bg-white/10 select-btn-animated cursor-pointer rounded-lg poppins font-semibold px-2 md:px-3 py-1 text-[.8rem] md:text-[1rem]"
|
class="hover:bg-lyx-widget-lighter/60 select-btn-animated cursor-pointer rounded-lg poppins font-regular px-2 md:px-3 py-1 text-[.8rem] md:text-[1rem]"
|
||||||
:class="{ 'bg-accent hover:!bg-accent': currentIndex == index }">
|
:class="{ 'bg-lyx-widget-lighter hover:!bg-lyx-widget-lighter': currentIndex == index }">
|
||||||
{{ opt.label }}
|
{{ opt.label }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
138
dashboard/components/analyst/ComposableChart.vue
Normal file
138
dashboard/components/analyst/ComposableChart.vue
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { ChartData, ChartOptions } from 'chart.js';
|
||||||
|
import { useLineChart, LineChart } from 'vue-chart-3';
|
||||||
|
import * as datefns from 'date-fns';
|
||||||
|
|
||||||
|
registerChartComponents();
|
||||||
|
|
||||||
|
const errored = ref<boolean>(false);
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
labels: string[],
|
||||||
|
title: string,
|
||||||
|
datasets: {
|
||||||
|
points: number[],
|
||||||
|
color: string,
|
||||||
|
chartType: string,
|
||||||
|
name: string
|
||||||
|
}[]
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const chartOptions = ref<ChartOptions<'line'>>({
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
interaction: {
|
||||||
|
intersect: false,
|
||||||
|
mode: 'nearest',
|
||||||
|
axis: 'x',
|
||||||
|
includeInvisible: true
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
ticks: { display: true },
|
||||||
|
grid: {
|
||||||
|
display: true,
|
||||||
|
drawBorder: false,
|
||||||
|
color: '#CCCCCC22',
|
||||||
|
// borderDash: [5, 10]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
ticks: { display: true },
|
||||||
|
grid: {
|
||||||
|
display: true,
|
||||||
|
drawBorder: false,
|
||||||
|
color: '#CCCCCC22',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: { display: true },
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: props.title
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
enabled: true,
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||||
|
titleFont: { size: 16, weight: 'bold' },
|
||||||
|
bodyFont: { size: 14 },
|
||||||
|
padding: 10,
|
||||||
|
cornerRadius: 4,
|
||||||
|
boxPadding: 10,
|
||||||
|
caretPadding: 20,
|
||||||
|
yAlign: 'bottom',
|
||||||
|
xAlign: 'center',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const chartData = ref<ChartData<'line'>>({
|
||||||
|
labels: props.labels.map(e => {
|
||||||
|
try {
|
||||||
|
return datefns.format(new Date(e), 'dd/MM');
|
||||||
|
} catch (ex) {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
datasets: props.datasets.map(e => ({
|
||||||
|
data: e.points,
|
||||||
|
label: e.name,
|
||||||
|
backgroundColor: [e.color + '77'],
|
||||||
|
borderColor: e.color,
|
||||||
|
borderWidth: 4,
|
||||||
|
fill: true,
|
||||||
|
tension: 0.45,
|
||||||
|
pointRadius: 0,
|
||||||
|
pointHoverRadius: 10,
|
||||||
|
hoverBackgroundColor: e.color,
|
||||||
|
hoverBorderColor: 'white',
|
||||||
|
hoverBorderWidth: 2,
|
||||||
|
type: e.chartType
|
||||||
|
} as any))
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const { lineChartProps, lineChartRef } = useLineChart({ chartData: chartData, options: chartOptions });
|
||||||
|
|
||||||
|
function createGradient(startColor: string) {
|
||||||
|
const c = document.createElement('canvas');
|
||||||
|
const ctx = c.getContext("2d");
|
||||||
|
let gradient: any = `${startColor}22`;
|
||||||
|
if (ctx) {
|
||||||
|
gradient = ctx.createLinearGradient(0, 25, 0, 300);
|
||||||
|
gradient.addColorStop(0, `${startColor}99`);
|
||||||
|
gradient.addColorStop(0.35, `${startColor}66`);
|
||||||
|
gradient.addColorStop(1, `${startColor}22`);
|
||||||
|
} else {
|
||||||
|
console.warn('Cannot get context for gradient');
|
||||||
|
}
|
||||||
|
|
||||||
|
return gradient;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
chartData.value.datasets.forEach(dataset => {
|
||||||
|
if (dataset.borderColor && dataset.borderColor.toString().startsWith('#')) {
|
||||||
|
dataset.backgroundColor = [createGradient(dataset.borderColor as string)]
|
||||||
|
} else {
|
||||||
|
dataset.backgroundColor = [createGradient('#3d59a4')]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (ex) {
|
||||||
|
errored.value = true;
|
||||||
|
console.error(ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="errored"> ERROR CREATING CHART </div>
|
||||||
|
<LineChart v-if="!errored" ref="lineChartRef" v-bind="lineChartProps"> </LineChart>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
110
dashboard/components/analyst/LineChart.vue
Normal file
110
dashboard/components/analyst/LineChart.vue
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { ChartData, ChartOptions } from 'chart.js';
|
||||||
|
import { useLineChart, LineChart } from 'vue-chart-3';
|
||||||
|
registerChartComponents();
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
data: any[],
|
||||||
|
labels: string[]
|
||||||
|
color: string,
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const chartOptions = ref<ChartOptions<'line'>>({
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
interaction: {
|
||||||
|
intersect: false,
|
||||||
|
mode: 'nearest',
|
||||||
|
axis: 'x',
|
||||||
|
includeInvisible: true
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
ticks: { display: true },
|
||||||
|
grid: {
|
||||||
|
display: true,
|
||||||
|
drawBorder: false,
|
||||||
|
color: '#CCCCCC22',
|
||||||
|
// borderDash: [5, 10]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
ticks: { display: true },
|
||||||
|
grid: {
|
||||||
|
display: true,
|
||||||
|
drawBorder: false,
|
||||||
|
color: '#CCCCCC22',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: { display: false },
|
||||||
|
title: { display: false },
|
||||||
|
tooltip: {
|
||||||
|
enabled: true,
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||||
|
titleFont: { size: 16, weight: 'bold' },
|
||||||
|
bodyFont: { size: 14 },
|
||||||
|
padding: 10,
|
||||||
|
cornerRadius: 4,
|
||||||
|
boxPadding: 10,
|
||||||
|
caretPadding: 20,
|
||||||
|
yAlign: 'bottom',
|
||||||
|
xAlign: 'center',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const chartData = ref<ChartData<'line'>>({
|
||||||
|
labels: props.labels,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
data: props.data,
|
||||||
|
backgroundColor: [props.color + '77'],
|
||||||
|
borderColor: props.color,
|
||||||
|
borderWidth: 4,
|
||||||
|
fill: true,
|
||||||
|
tension: 0.45,
|
||||||
|
pointRadius: 0,
|
||||||
|
pointHoverRadius: 10,
|
||||||
|
hoverBackgroundColor: props.color,
|
||||||
|
hoverBorderColor: 'white',
|
||||||
|
hoverBorderWidth: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const { lineChartProps, lineChartRef } = useLineChart({ chartData: chartData, options: chartOptions });
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
|
||||||
|
const c = document.createElement('canvas');
|
||||||
|
const ctx = c.getContext("2d");
|
||||||
|
let gradient: any = `${props.color}22`;
|
||||||
|
if (ctx) {
|
||||||
|
gradient = ctx.createLinearGradient(0, 25, 0, 300);
|
||||||
|
gradient.addColorStop(0, `${props.color}99`);
|
||||||
|
gradient.addColorStop(0.35, `${props.color}66`);
|
||||||
|
gradient.addColorStop(1, `${props.color}22`);
|
||||||
|
} else {
|
||||||
|
console.warn('Cannot get context for gradient');
|
||||||
|
}
|
||||||
|
|
||||||
|
chartData.value.datasets[0].backgroundColor = [gradient];
|
||||||
|
|
||||||
|
watch(props, () => {
|
||||||
|
chartData.value.labels = props.labels;
|
||||||
|
chartData.value.datasets[0].data = props.data;
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<LineChart ref="lineChartRef" v-bind="lineChartProps"> </LineChart>
|
||||||
|
</template>
|
||||||
329
dashboard/components/dashboard/ActionableChart.vue
Normal file
329
dashboard/components/dashboard/ActionableChart.vue
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted } from 'vue';
|
||||||
|
import DateService, { type Slice } from '@services/DateService';
|
||||||
|
import type { ChartData, ChartOptions, TooltipModel } from 'chart.js';
|
||||||
|
import { useLineChart, LineChart } from 'vue-chart-3';
|
||||||
|
registerChartComponents();
|
||||||
|
|
||||||
|
|
||||||
|
const chartOptions = ref<ChartOptions<'line'>>({
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
interaction: {
|
||||||
|
intersect: false,
|
||||||
|
mode: 'nearest',
|
||||||
|
axis: 'x',
|
||||||
|
includeInvisible: true
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
ticks: { display: true },
|
||||||
|
grid: {
|
||||||
|
display: true,
|
||||||
|
drawBorder: false,
|
||||||
|
color: '#CCCCCC22',
|
||||||
|
// borderDash: [5, 10]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
ticks: { display: true },
|
||||||
|
grid: {
|
||||||
|
display: true,
|
||||||
|
drawBorder: false,
|
||||||
|
color: '#CCCCCC22',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: { display: false },
|
||||||
|
title: { display: false },
|
||||||
|
tooltip: {
|
||||||
|
// enabled: true,
|
||||||
|
// backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||||
|
// titleFont: { size: 16, weight: 'bold' },
|
||||||
|
// bodyFont: { size: 14 },
|
||||||
|
// padding: 10,
|
||||||
|
// cornerRadius: 4,
|
||||||
|
// boxPadding: 10,
|
||||||
|
// caretPadding: 20,
|
||||||
|
// yAlign: 'bottom',
|
||||||
|
// xAlign: 'center',
|
||||||
|
enabled: false,
|
||||||
|
position: 'nearest',
|
||||||
|
external: externalTooltipHandler
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const chartData = ref<ChartData<'line' | 'bar' | 'bubble'>>({
|
||||||
|
labels: [],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Visits',
|
||||||
|
data: [],
|
||||||
|
backgroundColor: ['#5655d7'],
|
||||||
|
borderColor: '#5655d7',
|
||||||
|
borderWidth: 4,
|
||||||
|
fill: true,
|
||||||
|
tension: 0.45,
|
||||||
|
pointRadius: 0,
|
||||||
|
pointHoverRadius: 10,
|
||||||
|
hoverBackgroundColor: '#5655d7',
|
||||||
|
hoverBorderColor: 'white',
|
||||||
|
hoverBorderWidth: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Unique sessions',
|
||||||
|
data: [],
|
||||||
|
backgroundColor: ['#4abde8'],
|
||||||
|
borderColor: '#4abde8',
|
||||||
|
borderWidth: 2,
|
||||||
|
hoverBackgroundColor: '#4abde8',
|
||||||
|
hoverBorderColor: '#4abde8',
|
||||||
|
hoverBorderWidth: 2,
|
||||||
|
type: 'bar'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Events',
|
||||||
|
data: [],
|
||||||
|
backgroundColor: ['#fbbf24'],
|
||||||
|
borderColor: '#fbbf24',
|
||||||
|
borderWidth: 2,
|
||||||
|
hoverBackgroundColor: '#fbbf24',
|
||||||
|
hoverBorderColor: '#fbbf24',
|
||||||
|
hoverBorderWidth: 2,
|
||||||
|
type: 'bubble',
|
||||||
|
stack: 'combined'
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const { lineChartProps, lineChartRef, update: updateChart } = useLineChart({ chartData: (chartData as any), options: chartOptions });
|
||||||
|
|
||||||
|
const externalTooltipElement = ref<null | HTMLDivElement>(null);
|
||||||
|
|
||||||
|
function externalTooltipHandler(context: { chart: any, tooltip: TooltipModel<'line' | 'bar'> }) {
|
||||||
|
const { chart, tooltip } = context;
|
||||||
|
const tooltipEl = externalTooltipElement.value;
|
||||||
|
|
||||||
|
currentTooltipData.value.visits = (tooltip.dataPoints.find(e => e.datasetIndex == 0)?.raw) as number;
|
||||||
|
currentTooltipData.value.sessions = (tooltip.dataPoints.find(e => e.datasetIndex == 1)?.raw) as number;
|
||||||
|
currentTooltipData.value.events = ((tooltip.dataPoints.find(e => e.datasetIndex == 2)?.raw) as any)?.r2 as number;
|
||||||
|
|
||||||
|
currentTooltipData.value.date = new Date(allDatesFull.value[tooltip.dataPoints[0].dataIndex]).toLocaleDateString();
|
||||||
|
|
||||||
|
if (!tooltipEl) return;
|
||||||
|
if (tooltip.opacity === 0) {
|
||||||
|
tooltipEl.style.opacity = '0';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { left: positionX, top: positionY } = chart.canvas.getBoundingClientRect();
|
||||||
|
tooltipEl.style.opacity = '1';
|
||||||
|
tooltipEl.style.left = positionX + tooltip.caretX + 'px';
|
||||||
|
tooltipEl.style.top = positionY + tooltip.caretY + 'px';
|
||||||
|
tooltipEl.style.padding = tooltip.options.padding + 'px ' + tooltip.options.padding + 'px';
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const selectLabels: { label: string, value: Slice }[] = [
|
||||||
|
{ label: 'Hour', value: 'hour' },
|
||||||
|
{ label: 'Day', value: 'day' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const selectedLabelIndex = ref<number>(1);
|
||||||
|
|
||||||
|
|
||||||
|
const activeProject = useActiveProject();
|
||||||
|
|
||||||
|
const { safeSnapshotDates } = useSnapshot()
|
||||||
|
|
||||||
|
const allDatesFull = ref<string[]>([]);
|
||||||
|
|
||||||
|
function transformResponse(input: { _id: string, count: number }[]) {
|
||||||
|
const data = input.map(e => e.count);
|
||||||
|
const labels = input.map(e => DateService.getChartLabelFromISO(e._id, navigator.language, selectLabels[selectedLabelIndex.value].value));
|
||||||
|
allDatesFull.value = input.map(e => e._id.toString());
|
||||||
|
return { data, labels }
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = computed(() => {
|
||||||
|
return {
|
||||||
|
from: safeSnapshotDates.value.from,
|
||||||
|
to: safeSnapshotDates.value.to,
|
||||||
|
slice: selectLabels[selectedLabelIndex.value].value
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const visitsData = useFetch(`/api/metrics/${activeProject.value?._id}/timeline/visits`, {
|
||||||
|
method: 'POST', ...signHeaders({ v2: 'true' }), body, transform: transformResponse,
|
||||||
|
lazy: true, immediate: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const eventsData = useFetch(`/api/metrics/${activeProject.value?._id}/timeline/events`, {
|
||||||
|
method: 'POST', ...signHeaders({ v2: 'true' }), body, transform: transformResponse,
|
||||||
|
lazy: true, immediate: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const sessionsData = useFetch(`/api/metrics/${activeProject.value?._id}/timeline/sessions`, {
|
||||||
|
method: 'POST', ...signHeaders({ v2: 'true' }), body, transform: transformResponse,
|
||||||
|
lazy: true, immediate: false
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const readyToDisplay = computed(() => {
|
||||||
|
return !visitsData.pending.value && !eventsData.pending.value && !sessionsData.pending.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(readyToDisplay, () => {
|
||||||
|
if (readyToDisplay.value === true) onDataReady();
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
function createGradient(startColor: string) {
|
||||||
|
const c = document.createElement('canvas');
|
||||||
|
const ctx = c.getContext("2d");
|
||||||
|
let gradient: any = `${startColor}22`;
|
||||||
|
if (ctx) {
|
||||||
|
gradient = ctx.createLinearGradient(0, 25, 0, 300);
|
||||||
|
gradient.addColorStop(0, `${startColor}99`);
|
||||||
|
gradient.addColorStop(0.35, `${startColor}66`);
|
||||||
|
gradient.addColorStop(1, `${startColor}22`);
|
||||||
|
} else {
|
||||||
|
console.warn('Cannot get context for gradient');
|
||||||
|
}
|
||||||
|
|
||||||
|
return gradient;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDataReady() {
|
||||||
|
console.log('DATA READY');
|
||||||
|
|
||||||
|
if (!visitsData.data.value) return;
|
||||||
|
if (!eventsData.data.value) return;
|
||||||
|
if (!sessionsData.data.value) return;
|
||||||
|
|
||||||
|
console.log('DATA READY 2');
|
||||||
|
|
||||||
|
chartData.value.labels = visitsData.data.value.labels;
|
||||||
|
|
||||||
|
const maxChartY = Math.max(...visitsData.data.value.data, ...sessionsData.data.value.data);
|
||||||
|
const maxEventSize = Math.max(...eventsData.data.value.data)
|
||||||
|
|
||||||
|
chartData.value.datasets[0].data = visitsData.data.value.data;
|
||||||
|
chartData.value.datasets[1].data = sessionsData.data.value.data;
|
||||||
|
chartData.value.datasets[2].data = eventsData.data.value.data.map(e => {
|
||||||
|
const rValue = 25 / maxEventSize * e;
|
||||||
|
return { x: 0, y: maxChartY + 70, r: isNaN(rValue) ? 0 : rValue, r2: e }
|
||||||
|
});
|
||||||
|
|
||||||
|
chartData.value.datasets[0].backgroundColor = [createGradient('#5655d7')];
|
||||||
|
chartData.value.datasets[1].backgroundColor = [createGradient('#4abde8')];
|
||||||
|
chartData.value.datasets[2].backgroundColor = [createGradient('#fbbf24')];
|
||||||
|
|
||||||
|
console.log('UPDATE CHART');
|
||||||
|
updateChart();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentTooltipData = ref<{ visits: number, events: number, sessions: number, date: string }>({
|
||||||
|
visits: 0,
|
||||||
|
events: 0,
|
||||||
|
sessions: 0,
|
||||||
|
date: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const tooltipNameIndex = ['visits', 'sessions', 'events'];
|
||||||
|
|
||||||
|
function onLegendChange(dataset: any, index: number, checked: any) {
|
||||||
|
dataset.hidden = !checked;
|
||||||
|
}
|
||||||
|
|
||||||
|
const legendColors = [
|
||||||
|
'#5655d7',
|
||||||
|
'#4abde8',
|
||||||
|
'#fbbf24'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
visitsData.execute();
|
||||||
|
eventsData.execute();
|
||||||
|
sessionsData.execute();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CardTitled title="Trend chart" sub="Easily match Visits, Unique sessions and Events trends." class="w-full">
|
||||||
|
<template #header>
|
||||||
|
<SelectButton class="w-fit" @changeIndex="selectedLabelIndex = $event"
|
||||||
|
:currentIndex="selectedLabelIndex" :options="selectLabels">
|
||||||
|
</SelectButton>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="flex gap-6 w-full justify-between">
|
||||||
|
<LyxUiButton type="secondary" to="/analyst">
|
||||||
|
<div class="flex items-center gap-2 px-10">
|
||||||
|
<i class="far fa-sparkles text-yellow-400"></i>
|
||||||
|
<div class="poppins text-lyx-text"> Ask AI </div>
|
||||||
|
</div>
|
||||||
|
</LyxUiButton>
|
||||||
|
<div class="flex gap-6">
|
||||||
|
<div v-for="(dataset, index) of chartData.datasets" class="flex gap-2 items-center text-[.9rem]">
|
||||||
|
<UCheckbox :ui="{
|
||||||
|
color: `text-[${legendColors[index]}]`
|
||||||
|
}" :model-value="true" @change="onLegendChange(dataset, index, $event)"></UCheckbox>
|
||||||
|
<label class="mt-[2px]"> {{ dataset.label }} </label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div id='external-tooltip' ref="externalTooltipElement" class="z-[400]">
|
||||||
|
<LyxUiCard>
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
<div> Date: </div>
|
||||||
|
<div v-if="currentTooltipData"> {{ currentTooltipData.date }}</div>
|
||||||
|
</div>
|
||||||
|
<div v-for="(dataset, index) of chartData.datasets" class="flex gap-2 items-center">
|
||||||
|
<div :style="`background-color: ${legendColors[index]}`" class="h-4 w-4 rounded-full">
|
||||||
|
</div>
|
||||||
|
<div> {{ dataset.label }}</div>
|
||||||
|
<div v-if="currentTooltipData" class="grow text-right px-4">
|
||||||
|
{{ (currentTooltipData as any)[tooltipNameIndex[index]] }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- <div class="bg-lyx-background-lighter h-[2px] w-full my-2"> </div> -->
|
||||||
|
</LyxUiCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div v-if="!readyToDisplay" class="flex justify-center py-40">
|
||||||
|
<i class="fas fa-spinner text-[2rem] text-accent animate-[spin_1s_linear_infinite] duration-500"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-end" v-if="readyToDisplay">
|
||||||
|
<LineChart ref="lineChartRef" class="w-full h-full" v-bind="lineChartProps"> </LineChart>
|
||||||
|
</div>
|
||||||
|
</CardTitled>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
#external-tooltip {
|
||||||
|
border-radius: 3px;
|
||||||
|
color: white;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
transform: translate(-50%, 0);
|
||||||
|
transition: all .1s ease;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -46,7 +46,28 @@ const chartData = ref<ChartData<'doughnut'>>({
|
|||||||
{
|
{
|
||||||
rotation: 1,
|
rotation: 1,
|
||||||
data: [],
|
data: [],
|
||||||
backgroundColor: ['#6bbbe3', '#5655d0', '#a6d5cb', '#fae0b9'],
|
backgroundColor: [
|
||||||
|
"#5655d0",
|
||||||
|
"#6bbbe3",
|
||||||
|
"#a6d5cb",
|
||||||
|
"#fae0b9",
|
||||||
|
"#f28e8e",
|
||||||
|
"#e3a7e4",
|
||||||
|
"#c4a8e1",
|
||||||
|
"#8cc1d8",
|
||||||
|
"#f9c2cd",
|
||||||
|
"#b4e3b2",
|
||||||
|
"#ffdfba",
|
||||||
|
"#e9c3b5",
|
||||||
|
"#d5b8d6",
|
||||||
|
"#add7f6",
|
||||||
|
"#ffd1dc",
|
||||||
|
"#ffe7a1",
|
||||||
|
"#a8e6cf",
|
||||||
|
"#d4a5a5",
|
||||||
|
"#f3d6e4",
|
||||||
|
"#c3aed6"
|
||||||
|
],
|
||||||
borderColor: ['#1d1d1f'],
|
borderColor: ['#1d1d1f'],
|
||||||
borderWidth: 2
|
borderWidth: 2
|
||||||
},
|
},
|
||||||
@@ -87,7 +108,7 @@ const headers = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const eventsData = useFetch(`/api/metrics/${activeProject.value?._id}/data/events`, {
|
const eventsData = useFetch(`/api/metrics/${activeProject.value?._id}/data/events`, {
|
||||||
method: 'POST', headers, lazy: true, immediate: false,transform:transformResponse
|
method: 'POST', headers, lazy: true, immediate: false, transform: transformResponse
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -13,6 +13,19 @@ function copyProjectId() {
|
|||||||
navigator.clipboard.writeText((activeProject.value?._id || 0).toString());
|
navigator.clipboard.writeText((activeProject.value?._id || 0).toString());
|
||||||
createAlert('Success', 'Project id copied successfully.', 'far fa-circle-check', 5000);
|
createAlert('Success', 'Project id copied successfully.', 'far fa-circle-check', 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function showAnomalyInfoAlert() {
|
||||||
|
createAlert('AI Anomaly Detector info',
|
||||||
|
`Anomaly detector is running. It helps you detect a spike in visits or events, it could mean an
|
||||||
|
attack or simply higher traffic due to good performance. Additionally, it can detect if someone is
|
||||||
|
stealing parts of your website and hosting a duplicate version—an unfortunately common practice.
|
||||||
|
Litlyx will notify you via email with actionable advices`,
|
||||||
|
'far fa-bug',
|
||||||
|
10000
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
@@ -29,8 +42,10 @@ function copyProjectId() {
|
|||||||
|
|
||||||
<div class="flex md:gap-2 items-center md:justify-start flex-col md:flex-row">
|
<div class="flex md:gap-2 items-center md:justify-start flex-col md:flex-row">
|
||||||
<div class="poppins font-medium text-lyx-text-darker text-[1.2rem]">Project:</div>
|
<div class="poppins font-medium text-lyx-text-darker text-[1.2rem]">Project:</div>
|
||||||
<div class="text-lyx-text poppins font-medium text-[1.2rem]"> {{ activeProject?.name || 'Loading...' }} </div>
|
<div class="text-lyx-text poppins font-medium text-[1.2rem]"> {{ activeProject?.name || 'Loading...' }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col md:flex-row md:gap-2 items-center md:justify-start">
|
<div class="flex flex-col md:flex-row md:gap-2 items-center md:justify-start">
|
||||||
<div class="poppins font-medium text-lyx-text-darker text-[1.2rem]">Project id:</div>
|
<div class="poppins font-medium text-lyx-text-darker text-[1.2rem]">Project id:</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
@@ -38,9 +53,20 @@ function copyProjectId() {
|
|||||||
{{ activeProject?._id || 'Loading...' }}
|
{{ activeProject?._id || 'Loading...' }}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center ml-3">
|
<div class="flex items-center ml-3">
|
||||||
<i @click="copyProjectId()" class="far fa-copy text-lyx-text hover:text-lyx-primary cursor-pointer text-[1.2rem]"></i>
|
<i @click="copyProjectId()"
|
||||||
|
class="far fa-copy text-lyx-text hover:text-lyx-primary cursor-pointer text-[1.2rem]"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-2 items-center text-text/90 justify-center md:justify-start">
|
||||||
|
<div class="animate-pulse w-[1rem] h-[1rem] bg-green-400 rounded-full"> </div>
|
||||||
|
<div class="poppins font-regular text-[1rem]"> AI Anomaly Detector </div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="far fa-info-circle text-[.9rem] hover:text-lyx-primary cursor-pointer"
|
||||||
|
@click="showAnomalyInfoAlert"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -30,7 +30,29 @@ function transformResponse(input: { _id: string, name: string, count: number }[]
|
|||||||
});
|
});
|
||||||
|
|
||||||
const parsedDatasets: any[] = [];
|
const parsedDatasets: any[] = [];
|
||||||
const colors = ['#5655d0', '#6bbbe3', '#a6d5cb', '#fae0b9'];
|
|
||||||
|
const colors = [
|
||||||
|
"#5655d0",
|
||||||
|
"#6bbbe3",
|
||||||
|
"#a6d5cb",
|
||||||
|
"#fae0b9",
|
||||||
|
"#f28e8e",
|
||||||
|
"#e3a7e4",
|
||||||
|
"#c4a8e1",
|
||||||
|
"#8cc1d8",
|
||||||
|
"#f9c2cd",
|
||||||
|
"#b4e3b2",
|
||||||
|
"#ffdfba",
|
||||||
|
"#e9c3b5",
|
||||||
|
"#d5b8d6",
|
||||||
|
"#add7f6",
|
||||||
|
"#ffd1dc",
|
||||||
|
"#ffe7a1",
|
||||||
|
"#a8e6cf",
|
||||||
|
"#d4a5a5",
|
||||||
|
"#f3d6e4",
|
||||||
|
"#c3aed6"
|
||||||
|
];
|
||||||
|
|
||||||
for (let i = 0; i < fixed.allKeys.length; i++) {
|
for (let i = 0; i < fixed.allKeys.length; i++) {
|
||||||
const line: any = {
|
const line: any = {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ async function analyzeEvent() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<CardTitled title="Event User Flow"
|
<CardTitled title="Event User Flow"
|
||||||
sub="Track your user's journey from external links to custom events within your platform." class="w-full p-4">
|
sub="Track your user's journey from external links to in-app events, maintaining a complete view of their path from entry to engagement." class="w-full p-4">
|
||||||
|
|
||||||
<div class="p-2 flex flex-col gap-3">
|
<div class="p-2 flex flex-col gap-3">
|
||||||
<USelectMenu searchable searchable-placeholder="Search an event..." class="w-full"
|
<USelectMenu searchable searchable-placeholder="Search an event..." class="w-full"
|
||||||
|
|||||||
@@ -1,185 +1,201 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { PricingCardProp } from './PricingCardGeneric.vue';
|
import type { PricingCardProp } from './PricingCardGeneric.vue';
|
||||||
|
|
||||||
const props = defineProps<{ currentSub: number }>();
|
|
||||||
|
|
||||||
const freePricing: PricingCardProp[] = [
|
|
||||||
{
|
|
||||||
title: 'Free',
|
|
||||||
price: '€0 / mo',
|
|
||||||
subs: [
|
|
||||||
'Up to 5000 visits/events per month',
|
|
||||||
'CPM 0€ per visit/event'
|
|
||||||
],
|
|
||||||
features: [
|
|
||||||
'Email support',
|
|
||||||
'Unlimited domains',
|
|
||||||
'Unlimited reports',
|
|
||||||
'AI Tokens: 10',
|
|
||||||
'Server type: SHARED',
|
|
||||||
'Data retention: 2 Months'
|
|
||||||
],
|
|
||||||
cta: 'Start For Free now!',
|
|
||||||
active: props.currentSub == 0,
|
|
||||||
isDowngrade: props.currentSub > 0,
|
|
||||||
planId: 0
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const customPricing: PricingCardProp[] = [
|
const { data: planData, refresh: refreshPlanData } = useFetch('/api/project/plan', {
|
||||||
{
|
...signHeaders(),
|
||||||
title: 'Enterprise',
|
lazy: true
|
||||||
price: 'Custom',
|
});
|
||||||
subs: [
|
|
||||||
'Unlimited visits/events per month',
|
const activeProject = useActiveProject();
|
||||||
'Service Tailor-made on needs'
|
|
||||||
],
|
watch(activeProject, () => {
|
||||||
features: [
|
refreshPlanData();
|
||||||
'Priority support',
|
});
|
||||||
'Server type: DEDICATED',
|
|
||||||
'DB instance: DEDICATED',
|
|
||||||
'Dedicated operator',
|
function getPricingsData() {
|
||||||
'White label',
|
|
||||||
'Custom Data Aggregation'
|
const freePricing: PricingCardProp[] = [
|
||||||
],
|
{
|
||||||
cta: 'Let\'s Talk!',
|
title: 'Free',
|
||||||
link: 'mailto:help@litlyx.com',
|
price: '€0 / mo',
|
||||||
active: false,
|
subs: [
|
||||||
isDowngrade: false,
|
'Up to 5000 visits/events per month',
|
||||||
planId: -1
|
'CPM 0€ per visit/event'
|
||||||
}
|
],
|
||||||
]
|
features: [
|
||||||
|
'Email support',
|
||||||
|
'Unlimited domains',
|
||||||
|
'Unlimited reports',
|
||||||
|
'AI Tokens: 10',
|
||||||
|
'Server type: SHARED',
|
||||||
|
'Data retention: 2 Months'
|
||||||
|
],
|
||||||
|
cta: 'Start For Free now!',
|
||||||
|
active: (planData.value?.premium_type || 0) == 0,
|
||||||
|
isDowngrade: (planData.value?.premium_type || 0) > 0,
|
||||||
|
planId: 0
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const customPricing: PricingCardProp[] = [
|
||||||
|
{
|
||||||
|
title: 'Enterprise',
|
||||||
|
price: 'Custom',
|
||||||
|
subs: [
|
||||||
|
'Unlimited visits/events per month',
|
||||||
|
'Service Tailor-made on needs'
|
||||||
|
],
|
||||||
|
features: [
|
||||||
|
'Priority support',
|
||||||
|
'Server type: DEDICATED',
|
||||||
|
'DB instance: DEDICATED',
|
||||||
|
'Dedicated operator',
|
||||||
|
'White label',
|
||||||
|
'Custom Data Aggregation'
|
||||||
|
],
|
||||||
|
cta: 'Let\'s Talk!',
|
||||||
|
link: 'mailto:help@litlyx.com',
|
||||||
|
active: false,
|
||||||
|
isDowngrade: false,
|
||||||
|
planId: -1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const slidePricings: PricingCardProp[] = [
|
||||||
|
{
|
||||||
|
title: 'Incubation',
|
||||||
|
price: '€4,99 / mo',
|
||||||
|
subs: [
|
||||||
|
'Up to 50.000 visits/events per month',
|
||||||
|
'CPM 0,10€ per visit/event'
|
||||||
|
],
|
||||||
|
features: [
|
||||||
|
'Slack support',
|
||||||
|
'Unlimited domains',
|
||||||
|
'Unlimited reports',
|
||||||
|
'AI Tokens: 30',
|
||||||
|
'Server type: SHARED',
|
||||||
|
'Data retention: 6 Months'
|
||||||
|
],
|
||||||
|
cta: 'Go to Cloud Dashboard',
|
||||||
|
active: (planData.value?.premium_type || 0) == 101,
|
||||||
|
isDowngrade: (planData.value?.premium_type || 0) > 101,
|
||||||
|
planId: 101
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Acceleration',
|
||||||
|
price: '€9,99 / mo',
|
||||||
|
subs: [
|
||||||
|
'Up to 150.000 visits/events per month',
|
||||||
|
'CPM 0,06€ per visit/event'
|
||||||
|
],
|
||||||
|
features: [
|
||||||
|
'Slack support',
|
||||||
|
'Unlimited domains',
|
||||||
|
'Unlimited reports',
|
||||||
|
'AI Tokens: 100',
|
||||||
|
'Server type: SHARED',
|
||||||
|
'Data retention: 9 Months'
|
||||||
|
],
|
||||||
|
cta: 'Go to Cloud Dashboard',
|
||||||
|
active: (planData.value?.premium_type || 0) == 102,
|
||||||
|
isDowngrade: (planData.value?.premium_type || 0) > 102,
|
||||||
|
planId: 102
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Growth',
|
||||||
|
price: '€29,99 / mo',
|
||||||
|
subs: [
|
||||||
|
'Up to 500.000 visits/events per month',
|
||||||
|
'CPM 0,059€ per visit/event'
|
||||||
|
],
|
||||||
|
features: [
|
||||||
|
'Slack support',
|
||||||
|
'Unlimited domains',
|
||||||
|
'Unlimited reports',
|
||||||
|
'AI Tokens: 3.000',
|
||||||
|
'Server type: SHARED',
|
||||||
|
'Data retention: 1 Year'
|
||||||
|
],
|
||||||
|
cta: 'Go to Cloud Dashboard',
|
||||||
|
active: (planData.value?.premium_type || 0) == 103,
|
||||||
|
isDowngrade: (planData.value?.premium_type || 0) > 103,
|
||||||
|
planId: 103
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Expansion',
|
||||||
|
price: '€59,99 / mo',
|
||||||
|
subs: [
|
||||||
|
'Up to 1.000.000 visits/events per month',
|
||||||
|
'CPM 0,059€ per visit/event'
|
||||||
|
],
|
||||||
|
features: [
|
||||||
|
'Slack support',
|
||||||
|
'Unlimited domains',
|
||||||
|
'Unlimited reports',
|
||||||
|
'AI Tokens: 5.000',
|
||||||
|
'Server type: SHARED',
|
||||||
|
'Data retention: 1 Year'
|
||||||
|
],
|
||||||
|
cta: 'Go to Cloud Dashboard',
|
||||||
|
active: (planData.value?.premium_type || 0) == 104,
|
||||||
|
isDowngrade: (planData.value?.premium_type || 0) > 104,
|
||||||
|
planId: 104
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Scaling',
|
||||||
|
price: '€99,99 / mo',
|
||||||
|
subs: [
|
||||||
|
'Up to 2.500.000 visits/events per month',
|
||||||
|
'CPM 0,039€ per visit/event'
|
||||||
|
],
|
||||||
|
features: [
|
||||||
|
'Slack support',
|
||||||
|
'Unlimited domains',
|
||||||
|
'Unlimited reports',
|
||||||
|
'AI Tokens: 10.000',
|
||||||
|
'Server type: DEDICATED',
|
||||||
|
'Data retention: 2 Years'
|
||||||
|
],
|
||||||
|
cta: 'Go to Cloud Dashboard',
|
||||||
|
active: (planData.value?.premium_type || 0) == 105,
|
||||||
|
isDowngrade: (planData.value?.premium_type || 0) > 105,
|
||||||
|
planId: 105
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Unicorn',
|
||||||
|
price: '€149,99 / mo',
|
||||||
|
subs: [
|
||||||
|
'Up to 5.000.000 visits/events per month',
|
||||||
|
'CPM 0,029€ per visit/event'
|
||||||
|
],
|
||||||
|
features: [
|
||||||
|
'Slack support',
|
||||||
|
'Unlimited domains',
|
||||||
|
'Unlimited reports',
|
||||||
|
'AI Tokens: 20.000',
|
||||||
|
'Server type: DEDICATED',
|
||||||
|
'Data retention: 3 Years'
|
||||||
|
],
|
||||||
|
cta: 'Go to Cloud Dashboard',
|
||||||
|
active: (planData.value?.premium_type || 0) == 106,
|
||||||
|
isDowngrade: (planData.value?.premium_type || 0) > 106,
|
||||||
|
planId: 106
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
return { freePricing, customPricing, slidePricings }
|
||||||
|
}
|
||||||
|
|
||||||
const slidePricings: PricingCardProp[] = [
|
|
||||||
{
|
|
||||||
title: 'Incubation',
|
|
||||||
price: '€4,99 / mo',
|
|
||||||
subs: [
|
|
||||||
'Up to 50.000 visits/events per month',
|
|
||||||
'CPM 0,10€ per visit/event'
|
|
||||||
],
|
|
||||||
features: [
|
|
||||||
'Slack support',
|
|
||||||
'Unlimited domains',
|
|
||||||
'Unlimited reports',
|
|
||||||
'AI Tokens: 30',
|
|
||||||
'Server type: SHARED',
|
|
||||||
'Data retention: 6 Months'
|
|
||||||
],
|
|
||||||
cta: 'Go to Cloud Dashboard',
|
|
||||||
active: props.currentSub == 101,
|
|
||||||
isDowngrade: props.currentSub > 101,
|
|
||||||
planId: 101
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Acceleration',
|
|
||||||
price: '€9,99 / mo',
|
|
||||||
subs: [
|
|
||||||
'Up to 150.000 visits/events per month',
|
|
||||||
'CPM 0,06€ per visit/event'
|
|
||||||
],
|
|
||||||
features: [
|
|
||||||
'Slack support',
|
|
||||||
'Unlimited domains',
|
|
||||||
'Unlimited reports',
|
|
||||||
'AI Tokens: 100',
|
|
||||||
'Server type: SHARED',
|
|
||||||
'Data retention: 9 Months'
|
|
||||||
],
|
|
||||||
cta: 'Go to Cloud Dashboard',
|
|
||||||
active: props.currentSub == 102,
|
|
||||||
isDowngrade: props.currentSub > 102,
|
|
||||||
planId: 102
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Growth',
|
|
||||||
price: '€29,99 / mo',
|
|
||||||
subs: [
|
|
||||||
'Up to 500.000 visits/events per month',
|
|
||||||
'CPM 0,059€ per visit/event'
|
|
||||||
],
|
|
||||||
features: [
|
|
||||||
'Slack support',
|
|
||||||
'Unlimited domains',
|
|
||||||
'Unlimited reports',
|
|
||||||
'AI Tokens: 3.000',
|
|
||||||
'Server type: SHARED',
|
|
||||||
'Data retention: 1 Year'
|
|
||||||
],
|
|
||||||
cta: 'Go to Cloud Dashboard',
|
|
||||||
active: props.currentSub == 103,
|
|
||||||
isDowngrade: props.currentSub > 103,
|
|
||||||
planId: 103
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Expansion',
|
|
||||||
price: '€59,99 / mo',
|
|
||||||
subs: [
|
|
||||||
'Up to 1.000.000 visits/events per month',
|
|
||||||
'CPM 0,059€ per visit/event'
|
|
||||||
],
|
|
||||||
features: [
|
|
||||||
'Slack support',
|
|
||||||
'Unlimited domains',
|
|
||||||
'Unlimited reports',
|
|
||||||
'AI Tokens: 5.000',
|
|
||||||
'Server type: SHARED',
|
|
||||||
'Data retention: 1 Year'
|
|
||||||
],
|
|
||||||
cta: 'Go to Cloud Dashboard',
|
|
||||||
active: props.currentSub == 104,
|
|
||||||
isDowngrade: props.currentSub > 104,
|
|
||||||
planId: 104
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Scaling',
|
|
||||||
price: '€99,99 / mo',
|
|
||||||
subs: [
|
|
||||||
'Up to 2.500.000 visits/events per month',
|
|
||||||
'CPM 0,039€ per visit/event'
|
|
||||||
],
|
|
||||||
features: [
|
|
||||||
'Slack support',
|
|
||||||
'Unlimited domains',
|
|
||||||
'Unlimited reports',
|
|
||||||
'AI Tokens: 10.000',
|
|
||||||
'Server type: DEDICATED',
|
|
||||||
'Data retention: 2 Years'
|
|
||||||
],
|
|
||||||
cta: 'Go to Cloud Dashboard',
|
|
||||||
active: props.currentSub == 105,
|
|
||||||
isDowngrade: props.currentSub > 105,
|
|
||||||
planId: 105
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Unicorn',
|
|
||||||
price: '€149,99 / mo',
|
|
||||||
subs: [
|
|
||||||
'Up to 5.000.000 visits/events per month',
|
|
||||||
'CPM 0,029€ per visit/event'
|
|
||||||
],
|
|
||||||
features: [
|
|
||||||
'Slack support',
|
|
||||||
'Unlimited domains',
|
|
||||||
'Unlimited reports',
|
|
||||||
'AI Tokens: 20.000',
|
|
||||||
'Server type: DEDICATED',
|
|
||||||
'Data retention: 3 Years'
|
|
||||||
],
|
|
||||||
cta: 'Go to Cloud Dashboard',
|
|
||||||
active: props.currentSub == 106,
|
|
||||||
isDowngrade: props.currentSub > 106,
|
|
||||||
planId: 106
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
const emits = defineEmits<{
|
const emits = defineEmits<{
|
||||||
(evt: 'onCloseClick'): void
|
(evt: 'onCloseClick'): void
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const activeProject = useActiveProject()
|
|
||||||
|
|
||||||
async function onLifetimeUpgradeClick() {
|
async function onLifetimeUpgradeClick() {
|
||||||
const res = await $fetch<string>(`/api/pay/${activeProject.value?._id.toString()}/create-onetime`, {
|
const res = await $fetch<string>(`/api/pay/${activeProject.value?._id.toString()}/create-onetime`, {
|
||||||
...signHeaders({ 'content-type': 'application/json' }),
|
...signHeaders({ 'content-type': 'application/json' }),
|
||||||
@@ -201,9 +217,9 @@ async function onLifetimeUpgradeClick() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-8 mt-10 h-max xl:flex-row flex-col">
|
<div class="flex gap-8 mt-10 h-max xl:flex-row flex-col">
|
||||||
<PricingCardGeneric class="flex-1" :datas="freePricing"></PricingCardGeneric>
|
<PricingCardGeneric class="flex-1" :datas="getPricingsData().freePricing"></PricingCardGeneric>
|
||||||
<PricingCardGeneric class="flex-1" :datas="slidePricings" :default-index="2"></PricingCardGeneric>
|
<PricingCardGeneric class="flex-1" :datas="getPricingsData().slidePricings" :default-index="2"></PricingCardGeneric>
|
||||||
<PricingCardGeneric class="flex-1" :datas="customPricing"></PricingCardGeneric>
|
<PricingCardGeneric class="flex-1" :datas="getPricingsData().customPricing"></PricingCardGeneric>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<LyxUiCard class="w-full mt-6">
|
<LyxUiCard class="w-full mt-6">
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ const entries: SettingsTemplateEntry[] = [
|
|||||||
const activeProject = useActiveProject();
|
const activeProject = useActiveProject();
|
||||||
const projectNameInputVal = ref<string>(activeProject.value?.name || '');
|
const projectNameInputVal = ref<string>(activeProject.value?.name || '');
|
||||||
|
|
||||||
|
|
||||||
const apiKeys = ref<TApiSettings[]>([]);
|
const apiKeys = ref<TApiSettings[]>([]);
|
||||||
|
|
||||||
const newApiKeyName = ref<string>('');
|
const newApiKeyName = ref<string>('');
|
||||||
@@ -145,13 +144,15 @@ function copyProjectId() {
|
|||||||
<template #pname>
|
<template #pname>
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<LyxUiInput class="w-full px-4 py-2" v-model="projectNameInputVal"></LyxUiInput>
|
<LyxUiInput class="w-full px-4 py-2" v-model="projectNameInputVal"></LyxUiInput>
|
||||||
<LyxUiButton @click="changeProjectName()" :disabled="!canChange" type="primary"> Change </LyxUiButton>
|
<LyxUiButton v-if="!isGuest" @click="changeProjectName()" :disabled="!canChange" type="primary"> Change
|
||||||
|
</LyxUiButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #api>
|
<template #api>
|
||||||
<div class="flex items-center gap-4" v-if="apiKeys && apiKeys.length < 5">
|
<div class="flex items-center gap-4" v-if="apiKeys && apiKeys.length < 5">
|
||||||
<LyxUiInput class="grow px-4 py-2" placeholder="ApiKeyName" v-model="newApiKeyName"></LyxUiInput>
|
<LyxUiInput class="grow px-4 py-2" placeholder="ApiKeyName" v-model="newApiKeyName"></LyxUiInput>
|
||||||
<LyxUiButton @click="createApiKey()" :disabled="newApiKeyName.length < 3" type="primary">
|
<LyxUiButton v-if="!isGuest" @click="createApiKey()" :disabled="newApiKeyName.length < 3"
|
||||||
|
type="primary">
|
||||||
<i class="far fa-plus"></i>
|
<i class="far fa-plus"></i>
|
||||||
</LyxUiButton>
|
</LyxUiButton>
|
||||||
</div>
|
</div>
|
||||||
@@ -185,8 +186,8 @@ function copyProjectId() {
|
|||||||
<div><i class="far fa-copy" @click="copyScript()"></i></div>
|
<div><i class="far fa-copy" @click="copyScript()"></i></div>
|
||||||
</LyxUiCard>
|
</LyxUiCard>
|
||||||
</template>
|
</template>
|
||||||
<template #pdelete>
|
<template #pdelete >
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end" v-if="!isGuest">
|
||||||
<LyxUiButton type="danger" @click="deleteProject()">
|
<LyxUiButton type="danger" @click="deleteProject()">
|
||||||
Delete project
|
Delete project
|
||||||
</LyxUiButton>
|
</LyxUiButton>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import type { SettingsTemplateEntry } from './Template.vue';
|
import type { SettingsTemplateEntry } from './Template.vue';
|
||||||
|
import { getPlanFromId, PREMIUM_PLAN, type PREMIUM_TAG } from '@data/PREMIUM';
|
||||||
|
|
||||||
const activeProject = useActiveProject();
|
const activeProject = useActiveProject();
|
||||||
|
|
||||||
@@ -51,11 +52,17 @@ function openInvoice(link: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getPremiumName(type: number) {
|
function getPremiumName(type: number) {
|
||||||
if (type === 0) return 'FREE';
|
|
||||||
if (type === 1) return 'ACCELERATION';
|
|
||||||
if (type === 2) return 'EXPANSION';
|
|
||||||
return 'CUSTOM';
|
|
||||||
|
|
||||||
|
return Object.keys(PREMIUM_PLAN).map(e => ({
|
||||||
|
...PREMIUM_PLAN[e as PREMIUM_TAG], name: e
|
||||||
|
})).find(e => e.ID == type)?.name;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPremiumPrice(type: number) {
|
||||||
|
const PLAN = getPlanFromId(type);
|
||||||
|
if (!PLAN) return '0,00';
|
||||||
|
return (PLAN.COST / 100).toFixed(2).replace('.', ',')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -66,12 +73,17 @@ watch(activeProject, () => {
|
|||||||
|
|
||||||
|
|
||||||
const entries: SettingsTemplateEntry[] = [
|
const entries: SettingsTemplateEntry[] = [
|
||||||
|
// { id: 'info', title: 'Billing informations', text: 'Manage billing informations for this project' },
|
||||||
{ id: 'plan', title: 'Current plan', text: 'Manage current plat for this project' },
|
{ id: 'plan', title: 'Current plan', text: 'Manage current plat for this project' },
|
||||||
{ id: 'usage', title: 'Usage', text: 'Show usage of current project' },
|
{ id: 'usage', title: 'Usage', text: 'Show usage of current project' },
|
||||||
{ id: 'invoices', title: 'Invoices', text: 'Manage invoices of current project' },
|
{ id: 'invoices', title: 'Invoices', text: 'Manage invoices of current project' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
const currentBillingInfo = ref<any>({
|
||||||
|
address: ''
|
||||||
|
});
|
||||||
|
|
||||||
const { visible } = usePricingDrawer();
|
const { visible } = usePricingDrawer();
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
@@ -104,8 +116,12 @@ const { visible } = usePricingDrawer();
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<div class="poppins font-semibold text-[2rem]"> $0 </div>
|
<div class="poppins font-semibold text-[2rem]"> €
|
||||||
|
{{ getPremiumPrice(planData.premium_type) }} </div>
|
||||||
<div class="poppins text-text-sub mt-2"> per month </div>
|
<div class="poppins text-text-sub mt-2"> per month </div>
|
||||||
|
<div class="flex items-center ml-2">
|
||||||
|
<i class="far fa-info-circle text-[.8rem]"></i>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
|
|
||||||
import { Chart, registerables } from 'chart.js';
|
import { Chart, registerables } from 'chart.js';
|
||||||
|
import annotaionPlugin from 'chartjs-plugin-annotation';
|
||||||
|
|
||||||
let registered = false;
|
let registered = false;
|
||||||
export async function registerChartComponents() {
|
export async function registerChartComponents() {
|
||||||
if (registered) return;
|
if (registered) return;
|
||||||
if (process.client) {
|
if (process.client) {
|
||||||
Chart.register(...registerables);
|
Chart.register(...registerables, annotaionPlugin);
|
||||||
registered = true;
|
registered = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,7 @@ const sections: Section[] = [
|
|||||||
entries: [
|
entries: [
|
||||||
{ label: 'Dashboard', to: '/', icon: 'fal fa-table-layout' },
|
{ label: 'Dashboard', to: '/', icon: 'fal fa-table-layout' },
|
||||||
{ label: 'Events', to: '/events', icon: 'fal fa-square-bolt' },
|
{ label: 'Events', to: '/events', icon: 'fal fa-square-bolt' },
|
||||||
{ label: 'Analyst', to: '/analyst', icon: 'fal fa-microchip-ai' },
|
{ label: 'AI Analyst', to: '/analyst', icon: 'fal fa-sparkles' },
|
||||||
{ label: 'Insights (soon)', to: '#', icon: 'fal fa-lightbulb', disabled: true },
|
{ label: 'Insights (soon)', to: '#', icon: 'fal fa-lightbulb', disabled: true },
|
||||||
{ label: 'Links (soon)', to: '#', icon: 'fal fa-globe-pointer', disabled: true },
|
{ label: 'Links (soon)', to: '#', icon: 'fal fa-globe-pointer', disabled: true },
|
||||||
{ label: 'Integrations (soon)', to: '#', icon: 'fal fa-cube', disabled: true },
|
{ label: 'Integrations (soon)', to: '#', icon: 'fal fa-cube', disabled: true },
|
||||||
@@ -29,8 +29,10 @@ const sections: Section[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Slack support', icon: 'fab fa-slack',
|
label: 'Slack support', icon: 'fab fa-slack',
|
||||||
|
to:'#',
|
||||||
premiumOnly: true,
|
premiumOnly: true,
|
||||||
action() {
|
action() {
|
||||||
|
if (isGuest.value === true) return;
|
||||||
if (isPremium.value === true) {
|
if (isPremium.value === true) {
|
||||||
window.open('https://join.slack.com/t/litlyx/shared_invite/zt-2q3oawn29-hZlu_fBUBlc4052Ooe3FZg', '_blank');
|
window.open('https://join.slack.com/t/litlyx/shared_invite/zt-2q3oawn29-hZlu_fBUBlc4052Ooe3FZg', '_blank');
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ export default defineNuxtConfig({
|
|||||||
AUTH_JWT_SECRET: process.env.AUTH_JWT_SECRET,
|
AUTH_JWT_SECRET: process.env.AUTH_JWT_SECRET,
|
||||||
GOOGLE_AUTH_CLIENT_ID: process.env.GOOGLE_AUTH_CLIENT_ID,
|
GOOGLE_AUTH_CLIENT_ID: process.env.GOOGLE_AUTH_CLIENT_ID,
|
||||||
GOOGLE_AUTH_CLIENT_SECRET: process.env.GOOGLE_AUTH_CLIENT_SECRET,
|
GOOGLE_AUTH_CLIENT_SECRET: process.env.GOOGLE_AUTH_CLIENT_SECRET,
|
||||||
|
GITHUB_AUTH_CLIENT_ID: process.env.GITHUB_AUTH_CLIENT_ID,
|
||||||
|
GITHUB_AUTH_CLIENT_SECRET: process.env.GITHUB_AUTH_CLIENT_SECRET,
|
||||||
STRIPE_SECRET: process.env.STRIPE_SECRET,
|
STRIPE_SECRET: process.env.STRIPE_SECRET,
|
||||||
STRIPE_WH_SECRET: process.env.STRIPE_WH_SECRET,
|
STRIPE_WH_SECRET: process.env.STRIPE_WH_SECRET,
|
||||||
STRIPE_SECRET_TEST: process.env.STRIPE_SECRET_TEST,
|
STRIPE_SECRET_TEST: process.env.STRIPE_SECRET_TEST,
|
||||||
@@ -50,7 +52,8 @@ export default defineNuxtConfig({
|
|||||||
NOAUTH_USER_EMAIL: process.env.NOAUTH_USER_EMAIL,
|
NOAUTH_USER_EMAIL: process.env.NOAUTH_USER_EMAIL,
|
||||||
NOAUTH_USER_NAME: process.env.NOAUTH_USER_NAME,
|
NOAUTH_USER_NAME: process.env.NOAUTH_USER_NAME,
|
||||||
public: {
|
public: {
|
||||||
AUTH_MODE: process.env.AUTH_MODE
|
AUTH_MODE: process.env.AUTH_MODE,
|
||||||
|
GITHUB_CLIENT_ID: process.env.GITHUB_AUTH_CLIENT_ID || 'NONE'
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"@getbrevo/brevo": "^2.2.0",
|
"@getbrevo/brevo": "^2.2.0",
|
||||||
"@nuxtjs/tailwindcss": "^6.12.0",
|
"@nuxtjs/tailwindcss": "^6.12.0",
|
||||||
"chart.js": "^3.9.1",
|
"chart.js": "^3.9.1",
|
||||||
|
"chartjs-plugin-annotation": "^2.2.1",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"dayjs": "^1.11.11",
|
"dayjs": "^1.11.11",
|
||||||
"google-auth-library": "^9.9.0",
|
"google-auth-library": "^9.9.0",
|
||||||
@@ -25,7 +26,7 @@
|
|||||||
"nodemailer": "^6.9.13",
|
"nodemailer": "^6.9.13",
|
||||||
"nuxt": "^3.11.2",
|
"nuxt": "^3.11.2",
|
||||||
"nuxt-vue3-google-signin": "^0.0.11",
|
"nuxt-vue3-google-signin": "^0.0.11",
|
||||||
"openai": "^4.47.1",
|
"openai": "^4.61.0",
|
||||||
"pdfkit": "^0.15.0",
|
"pdfkit": "^0.15.0",
|
||||||
"primevue": "^3.52.0",
|
"primevue": "^3.52.0",
|
||||||
"redis": "^4.6.13",
|
"redis": "^4.6.13",
|
||||||
@@ -34,11 +35,14 @@
|
|||||||
"v-calendar": "^3.1.2",
|
"v-calendar": "^3.1.2",
|
||||||
"vue": "^3.4.21",
|
"vue": "^3.4.21",
|
||||||
"vue-chart-3": "^3.1.8",
|
"vue-chart-3": "^3.1.8",
|
||||||
"vue-router": "^4.3.0"
|
"vue-markdown-render": "^2.2.1",
|
||||||
|
"vue-router": "^4.3.0",
|
||||||
|
"winston": "^3.14.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nuxt/ui": "^2.15.2",
|
"@nuxt/ui": "^2.15.2",
|
||||||
"@types/jsonwebtoken": "^9.0.6",
|
"@types/jsonwebtoken": "^9.0.6",
|
||||||
|
"@types/markdown-it": "^14.1.2",
|
||||||
"@types/nodemailer": "^6.4.15",
|
"@types/nodemailer": "^6.4.15",
|
||||||
"@types/pdfkit": "^0.13.4",
|
"@types/pdfkit": "^0.13.4",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
definePageMeta({ layout: 'dashboard' });
|
|
||||||
|
|
||||||
|
import VueMarkdown from 'vue-markdown-render';
|
||||||
|
|
||||||
|
definePageMeta({ layout: 'dashboard' });
|
||||||
|
|
||||||
const activeProject = useActiveProject();
|
const activeProject = useActiveProject();
|
||||||
|
|
||||||
const { data: chatsList, refresh: reloadChatsList } = useFetch(`/api/ai/${activeProject.value?._id}/chats_list`, signHeaders());
|
const { data: chatsList, refresh: reloadChatsList } = useFetch(`/api/ai/${activeProject.value?._id}/chats_list`, {
|
||||||
|
...signHeaders()
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const viewChatsList = computed(() => (chatsList.value || []).toReversed());
|
||||||
|
|
||||||
const { data: chatsRemaining, refresh: reloadChatsRemaining } = useFetch(`/api/ai/${activeProject.value?._id}/chats_remaining`, signHeaders());
|
const { data: chatsRemaining, refresh: reloadChatsRemaining } = useFetch(`/api/ai/${activeProject.value?._id}/chats_remaining`, signHeaders());
|
||||||
|
|
||||||
@@ -12,7 +20,7 @@ const currentText = ref<string>("");
|
|||||||
const loading = ref<boolean>(false);
|
const loading = ref<boolean>(false);
|
||||||
|
|
||||||
const currentChatId = ref<string>("");
|
const currentChatId = ref<string>("");
|
||||||
const currentChatMessages = ref<any[]>([]);
|
const currentChatMessages = ref<{ role: string, content: string, charts?: any[] }[]>([]);
|
||||||
|
|
||||||
const scroller = ref<HTMLDivElement | null>(null);
|
const scroller = ref<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
@@ -39,7 +47,7 @@ async function sendMessage() {
|
|||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
...signHeaders({ 'Content-Type': 'application/json' })
|
...signHeaders({ 'Content-Type': 'application/json' })
|
||||||
});
|
});
|
||||||
currentChatMessages.value.push({ role: 'assistant', content: res });
|
currentChatMessages.value.push({ role: 'assistant', content: res.content || 'nocontent', charts: res.charts.map(e => JSON.parse(e)) });
|
||||||
|
|
||||||
await reloadChatsRemaining();
|
await reloadChatsRemaining();
|
||||||
await reloadChatsList();
|
await reloadChatsList();
|
||||||
@@ -67,15 +75,18 @@ async function sendMessage() {
|
|||||||
async function openChat(chat_id?: string) {
|
async function openChat(chat_id?: string) {
|
||||||
menuOpen.value = false;
|
menuOpen.value = false;
|
||||||
if (!activeProject.value) return;
|
if (!activeProject.value) return;
|
||||||
|
|
||||||
|
currentChatMessages.value = [];
|
||||||
|
|
||||||
if (!chat_id) {
|
if (!chat_id) {
|
||||||
currentChatMessages.value = [];
|
|
||||||
currentChatId.value = '';
|
currentChatId.value = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
currentChatId.value = chat_id;
|
currentChatId.value = chat_id;
|
||||||
const messages = await $fetch(`/api/ai/${activeProject.value._id}/${chat_id}/get_messages`, signHeaders());
|
const messages = await $fetch(`/api/ai/${activeProject.value._id}/${chat_id}/get_messages`, signHeaders());
|
||||||
if (!messages) return;
|
if (!messages) return;
|
||||||
currentChatMessages.value = messages;
|
|
||||||
|
currentChatMessages.value = messages.map(e => ({ ...e, charts: e.charts.map(k => JSON.parse(k)) })) as any;
|
||||||
setTimeout(() => scrollToBottom(), 1);
|
setTimeout(() => scrollToBottom(), 1);
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -99,10 +110,10 @@ function onKeyDown(e: KeyboardEvent) {
|
|||||||
const menuOpen = ref<boolean>(false);
|
const menuOpen = ref<boolean>(false);
|
||||||
|
|
||||||
const defaultPrompts = [
|
const defaultPrompts = [
|
||||||
'How many visits i got last week ?',
|
"Create a line chart with this data: \n[100, 200, 30, 300, 500, 40]",
|
||||||
'How many visits i got last month ?',
|
"Create a chart with Events (bar) and Visits (line) data from last week.",
|
||||||
'How many visits i got today ?',
|
"How many visits did I get last week?",
|
||||||
'How many events i got last week ?',
|
"Create a line chart of last week's visits."
|
||||||
]
|
]
|
||||||
|
|
||||||
async function deleteChat(chat_id: string) {
|
async function deleteChat(chat_id: string) {
|
||||||
@@ -117,6 +128,8 @@ async function deleteChat(chat_id: string) {
|
|||||||
await reloadChatsList();
|
await reloadChatsList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { visible: pricingDrawerVisible } = usePricingDrawer()
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -124,7 +137,7 @@ async function deleteChat(chat_id: string) {
|
|||||||
|
|
||||||
<div class="flex flex-row h-full">
|
<div class="flex flex-row h-full">
|
||||||
|
|
||||||
<div class="flex-[5] py-8 flex flex-col items-center relative">
|
<div class="flex-[5] py-8 flex flex-col items-center relative bg-lyx-background-light">
|
||||||
|
|
||||||
<div class="flex flex-col items-center mt-[20vh] px-28" v-if="currentChatMessages.length == 0">
|
<div class="flex flex-col items-center mt-[20vh] px-28" v-if="currentChatMessages.length == 0">
|
||||||
<div class="w-[10rem]">
|
<div class="w-[10rem]">
|
||||||
@@ -138,7 +151,7 @@ async function deleteChat(chat_id: string) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2 gap-4 mt-6" v-if="!isGuest">
|
<div class="grid grid-cols-2 gap-4 mt-6" v-if="!isGuest">
|
||||||
<div v-for="prompt of defaultPrompts" @click="currentText = prompt"
|
<div v-for="prompt of defaultPrompts" @click="currentText = prompt"
|
||||||
class="bg-[#2f2f2f] hover:bg-[#424242] cursor-pointer p-4 rounded-lg poppins text-center">
|
class="bg-lyx-widget-light hover:bg-lyx-widget-lighter cursor-pointer p-4 rounded-lg poppins text-center whitespace-pre-wrap flex items-center justify-center text-[.9rem]">
|
||||||
{{ prompt }}
|
{{ prompt }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -146,21 +159,36 @@ async function deleteChat(chat_id: string) {
|
|||||||
|
|
||||||
<div ref="scroller" class="flex flex-col w-full gap-6 px-6 xl:px-28 overflow-y-auto pb-20">
|
<div ref="scroller" class="flex flex-col w-full gap-6 px-6 xl:px-28 overflow-y-auto pb-20">
|
||||||
|
|
||||||
<div class="flex w-full" v-for="message of currentChatMessages">
|
<div class="flex w-full flex-col" v-for="message of currentChatMessages">
|
||||||
|
|
||||||
<div class="flex justify-end w-full poppins text-[1.1rem]" v-if="message.role === 'user'">
|
<div class="flex justify-end w-full poppins text-[1.1rem]" v-if="message.role === 'user'">
|
||||||
<div class="bg-[#303030] px-5 py-3 rounded-lg">
|
<div class="bg-lyx-widget-light px-5 py-3 rounded-lg">
|
||||||
{{ message.content }}
|
{{ message.content }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3 justify-start w-full poppins text-[1.1rem]"
|
<div class="flex items-center gap-3 justify-start w-full poppins text-[1.1rem]"
|
||||||
v-if="message.role === 'assistant'">
|
v-if="message.role === 'assistant' && message.content">
|
||||||
<div class="flex items-center justify-center shrink-0">
|
<div class="flex items-center justify-center shrink-0">
|
||||||
<img class="h-[3.5rem] w-auto" :src="'analyst.png'">
|
<img class="h-[3.5rem] w-auto" :src="'analyst.png'">
|
||||||
</div>
|
</div>
|
||||||
<div v-html="parseMessageContent(message.content)"
|
<div class="max-w-[70%] text-text/90 ai-message">
|
||||||
class="max-w-[70%] text-text/90 whitespace-pre-wrap">
|
<vue-markdown :source="message.content" :options="{
|
||||||
|
html: true,
|
||||||
|
breaks: true,
|
||||||
|
}" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="message.charts && message.charts.length > 0"
|
||||||
|
class="flex items-center gap-3 justify-start w-full poppins text-[1.1rem] flex-col mt-4">
|
||||||
|
<div v-for="chart of message.charts" class="w-full">
|
||||||
|
<AnalystComposableChart :datasets="chart.datasets" :labels="chart.labels"
|
||||||
|
:title="chart.title">
|
||||||
|
</AnalystComposableChart>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="loading"
|
<div v-if="loading"
|
||||||
@@ -177,13 +205,13 @@ async function deleteChat(chat_id: string) {
|
|||||||
|
|
||||||
<div v-if="!isGuest" class="flex gap-2 items-center absolute bottom-8 left-0 w-full px-10 xl:px-28">
|
<div v-if="!isGuest" class="flex gap-2 items-center absolute bottom-8 left-0 w-full px-10 xl:px-28">
|
||||||
<input @keydown="onKeyDown" v-model="currentText"
|
<input @keydown="onKeyDown" v-model="currentText"
|
||||||
class="bg-[#303030] w-full focus:outline-none px-4 py-2 rounded-lg" type="text">
|
class="bg-lyx-widget-light w-full focus:outline-none px-4 py-2 rounded-lg" type="text">
|
||||||
<div @click="sendMessage()"
|
<div @click="sendMessage()"
|
||||||
class="bg-[#303030] hover:bg-[#464646] cursor-pointer px-4 py-2 rounded-full">
|
class="bg-lyx-widget-light hhover:bg-lyx-widget-lighter cursor-pointer px-4 py-2 rounded-full">
|
||||||
<i class="far fa-arrow-up"></i>
|
<i class="far fa-arrow-up"></i>
|
||||||
</div>
|
</div>
|
||||||
<div @click="menuOpen = !menuOpen"
|
<div @click="menuOpen = !menuOpen"
|
||||||
class="bg-[#303030] lg:hidden hover:bg-[#464646] cursor-pointer px-4 py-2 rounded-full">
|
class="bg-lyx-widget-light lg:hidden hhover:bg-lyx-widget-lighter cursor-pointer px-4 py-2 rounded-full">
|
||||||
<i class="far fa-message"></i>
|
<i class="far fa-message"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -194,32 +222,37 @@ async function deleteChat(chat_id: string) {
|
|||||||
<div :class="{
|
<div :class="{
|
||||||
'absolute': menuOpen,
|
'absolute': menuOpen,
|
||||||
'hidden lg:flex': !menuOpen
|
'hidden lg:flex': !menuOpen
|
||||||
}" class="flex-[2] bg-[#303030] p-6 flex flex-col gap-4 h-full overflow-hidden">
|
}" class="flex-[2] bg-lyx-widget-light p-6 flex flex-col gap-4 h-full overflow-hidden">
|
||||||
|
|
||||||
<div class="gap-2 flex flex-col">
|
<div class="gap-2 flex flex-col">
|
||||||
<div class="lg:hidden absolute right-4 top-4 text-[1.5rem]">
|
<div class="lg:hidden absolute right-4 top-4 text-[1.5rem]">
|
||||||
<i @click="menuOpen = false" class="fas fa-close cursor-pointer"></i>
|
<i @click="menuOpen = false" class="fas fa-close cursor-pointer"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="poppins font-semibold text-[1.5rem]">
|
<div class="poppins font-semibold text-[1.5rem]">
|
||||||
Lit, your AI Analyst is here!
|
What Lit can do for you?
|
||||||
</div>
|
</div>
|
||||||
<div class="poppins text-text/75">
|
<div class="poppins text-text/75">
|
||||||
Ask anything you want on your analytics,
|
Ask anything from your data history, visualize and overlap charts, explore events or metadata,
|
||||||
and understand more Trends and Key Points to take Strategic moves!
|
and enjoy a highly personalized data analysis experience.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-2 items-center py-3">
|
<div class="flex gap-2 items-center pt-3">
|
||||||
<div class="bg-accent w-5 h-5 rounded-full animate-pulse">
|
<div class="bg-accent w-5 h-5 rounded-full animate-pulse">
|
||||||
</div>
|
</div>
|
||||||
<div class="manrope font-semibold"> {{ chatsRemaining }} remaining messages </div>
|
<div class="manrope font-semibold"> {{ chatsRemaining }} remaining AI requests </div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="poppins font-semibold text-[1.1rem]"> History: </div>
|
<LyxUiButton type="primary" class="text-[.9rem] text-center w-full"
|
||||||
|
@click="pricingDrawerVisible = true">
|
||||||
|
Upgrade plan for more requests
|
||||||
|
</LyxUiButton>
|
||||||
|
|
||||||
|
<div class="poppins font-semibold text-[1.1rem]"> History </div>
|
||||||
|
|
||||||
<div class="px-2">
|
<div class="px-2">
|
||||||
<div @click="openChat()"
|
<div @click="openChat()"
|
||||||
class="bg-menu cursor-pointer hover:bg-menu/80 rounded-lg px-4 py-3 poppins flex gap-2 items-center">
|
class="bg-lyx-widget-lighter cursor-pointer hover:bg-lyx-widget rounded-lg px-4 py-3 poppins flex gap-4 items-center">
|
||||||
<div> <i class="fas fa-plus"></i> </div>
|
<div> <i class="fas fa-plus"></i> </div>
|
||||||
<div> New chat </div>
|
<div> New chat </div>
|
||||||
</div>
|
</div>
|
||||||
@@ -228,12 +261,13 @@ async function deleteChat(chat_id: string) {
|
|||||||
|
|
||||||
<div class="overflow-y-auto">
|
<div class="overflow-y-auto">
|
||||||
<div class="flex flex-col gap-2 px-2">
|
<div class="flex flex-col gap-2 px-2">
|
||||||
<div class="flex items-center gap-4 w-full" v-for="chat of chatsList?.toReversed()">
|
<div :class="{ '!bg-accent/60': chat._id.toString() === currentChatId }"
|
||||||
|
class="flex rounded-lg items-center gap-4 w-full px-4 bg-lyx-widget-lighter hover:bg-lyx-widget"
|
||||||
|
v-for="chat of viewChatsList">
|
||||||
<i @click="deleteChat(chat._id.toString())"
|
<i @click="deleteChat(chat._id.toString())"
|
||||||
class="fas fa-trash hover:text-gray-300 cursor-pointer"></i>
|
class="far fa-trash hover:text-gray-300 cursor-pointer"></i>
|
||||||
<div @click="openChat(chat._id.toString())"
|
<div @click="openChat(chat._id.toString())"
|
||||||
class="bg-menu px-4 py-3 w-full cursor-pointer hover:bg-menu/80 poppins rounded-lg"
|
class="py-3 w-full cursor-pointer poppins rounded-lg">
|
||||||
:class="{ '!bg-accent/60': chat._id.toString() === currentChatId }">
|
|
||||||
{{ chat.title }}
|
{{ chat.title }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -248,4 +282,76 @@ async function deleteChat(chat_id: string) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.ai-message {
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-top: 1.5em;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
p {
|
||||||
|
line-height: 1.8;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
max-width: 750px;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
margin: 1.5em 10px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
color: #555;
|
||||||
|
border-left: 5px solid #ccc;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
background-color: #f1f1f1;
|
||||||
|
padding: 2px 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul,
|
||||||
|
ol {
|
||||||
|
margin-left: 30px;
|
||||||
|
margin-bottom: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #007acc;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
margin: 2em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -78,6 +78,10 @@ const { snapshot } = useSnapshot();
|
|||||||
|
|
||||||
const refreshKey = computed(() => `${snapshot.value._id.toString() + activeProject.value?._id.toString()}`);
|
const refreshKey = computed(() => `${snapshot.value._id.toString() + activeProject.value?._id.toString()}`);
|
||||||
|
|
||||||
|
const isPremium = computed(() => {
|
||||||
|
return activeProject.value?.premium;
|
||||||
|
})
|
||||||
|
|
||||||
const pricingDrawer = usePricingDrawer();
|
const pricingDrawer = usePricingDrawer();
|
||||||
|
|
||||||
function goToUpgrade() {
|
function goToUpgrade() {
|
||||||
@@ -93,9 +97,7 @@ function goToUpgrade() {
|
|||||||
|
|
||||||
<div :key="'home-' + isLiveDemo()" v-if="projects && activeProject && firstInteraction.data.value">
|
<div :key="'home-' + isLiveDemo()" v-if="projects && activeProject && firstInteraction.data.value">
|
||||||
|
|
||||||
<div class="w-full px-4 py-2">
|
<div class="w-full px-4 py-2 gap-2 flex flex-col">
|
||||||
|
|
||||||
|
|
||||||
<div v-if="limitsInfo && limitsInfo.limited"
|
<div v-if="limitsInfo && limitsInfo.limited"
|
||||||
class="w-full bg-[#fbbf2422] p-4 rounded-lg text-[.9rem] flex items-center">
|
class="w-full bg-[#fbbf2422] p-4 rounded-lg text-[.9rem] flex items-center">
|
||||||
<div class="flex flex-col grow">
|
<div class="flex flex-col grow">
|
||||||
@@ -110,15 +112,35 @@ function goToUpgrade() {
|
|||||||
<div>
|
<div>
|
||||||
<LyxUiButton type="outline" @click="goToUpgrade()"> Upgrade </LyxUiButton>
|
<LyxUiButton type="outline" @click="goToUpgrade()"> Upgrade </LyxUiButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="!isPremium" class="w-full bg-[#5680f822] p-4 rounded-lg text-[.9rem] flex items-center">
|
||||||
|
<div class="flex flex-col grow">
|
||||||
|
<div class="poppins font-semibold text-lyx-primary">
|
||||||
|
Launch offer: 25% off
|
||||||
|
</div>
|
||||||
|
<div class="poppins text-lyx-primary">
|
||||||
|
We're offering an exclusive 25% discount forever on all plans starting from the Acceleration
|
||||||
|
Plan for our first 100 users who believe in our project.
|
||||||
|
<br>
|
||||||
|
Redeem Code: <span class="text-white font-bold text-[1rem]">LIT25</span> at checkout to
|
||||||
|
claim your discount.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<LyxUiButton type="outline" @click="goToUpgrade()"> Upgrade </LyxUiButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DashboardTopSection></DashboardTopSection>
|
<DashboardTopSection></DashboardTopSection>
|
||||||
<DashboardTopCards :key="refreshKey"></DashboardTopCards>
|
<DashboardTopCards :key="refreshKey"></DashboardTopCards>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="mt-6 px-6 flex gap-6 flex-col 2xl:flex-row w-full">
|
||||||
|
<DashboardActionableChart :key="refreshKey"></DashboardActionableChart>
|
||||||
|
</div>
|
||||||
|
<!--
|
||||||
<div class="mt-6 px-6 flex gap-6 flex-col 2xl:flex-row">
|
<div class="mt-6 px-6 flex gap-6 flex-col 2xl:flex-row">
|
||||||
|
|
||||||
<CardTitled :key="refreshKey" class="p-4 flex-1 w-full" title="Visits trends"
|
<CardTitled :key="refreshKey" class="p-4 flex-1 w-full" title="Visits trends"
|
||||||
@@ -128,26 +150,25 @@ function goToUpgrade() {
|
|||||||
:options="selectLabels">
|
:options="selectLabels">
|
||||||
</SelectButton>
|
</SelectButton>
|
||||||
</template>
|
</template>
|
||||||
<div>
|
<div>
|
||||||
<DashboardVisitsLineChart :slice="(selectLabels[mainChartSelectIndex].value as any)">
|
<DashboardVisitsLineChart :slice="(selectLabels[mainChartSelectIndex].value as any)">
|
||||||
</DashboardVisitsLineChart>
|
</DashboardVisitsLineChart>
|
||||||
</div>
|
</div>
|
||||||
</CardTitled>
|
</CardTitled>
|
||||||
|
|
||||||
<!-- <CardTitled :key="refreshKey" class="p-4 flex-1 w-full" title="Sessions"
|
<CardTitled :key="refreshKey" class="p-4 flex-1 w-full" title="Sessions" sub="Shows trends in sessions.">
|
||||||
sub="Shows trends in sessions.">
|
<template #header>
|
||||||
<template #header>
|
|
||||||
<SelectButton @changeIndex="sessionsChartSelectIndex = $event"
|
<SelectButton @changeIndex="sessionsChartSelectIndex = $event"
|
||||||
:currentIndex="sessionsChartSelectIndex" :options="selectLabels">
|
:currentIndex="sessionsChartSelectIndex" :options="selectLabels">
|
||||||
</SelectButton>
|
</SelectButton>
|
||||||
</template>
|
</template>
|
||||||
<div>
|
<div>
|
||||||
<DashboardSessionsLineChart :slice="(selectLabels[sessionsChartSelectIndex].value as any)">
|
<DashboardSessionsLineChart :slice="(selectLabels[sessionsChartSelectIndex].value as any)">
|
||||||
</DashboardSessionsLineChart>
|
</DashboardSessionsLineChart>
|
||||||
</div>
|
</div>
|
||||||
</CardTitled> -->
|
</CardTitled>
|
||||||
|
|
||||||
</div>
|
</div> -->
|
||||||
|
|
||||||
<div class="flex w-full justify-center mt-6 px-6">
|
<div class="flex w-full justify-center mt-6 px-6">
|
||||||
<div class="flex w-full gap-6 flex-col xl:flex-row">
|
<div class="flex w-full gap-6 flex-col xl:flex-row">
|
||||||
@@ -160,7 +181,6 @@ function goToUpgrade() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="flex w-full justify-center mt-6 px-6">
|
<div class="flex w-full justify-center mt-6 px-6">
|
||||||
<div class="flex w-full gap-6 flex-col xl:flex-row">
|
<div class="flex w-full gap-6 flex-col xl:flex-row">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
|
|||||||
@@ -81,6 +81,33 @@ function handleOnError(errorResponse: any) {
|
|||||||
alert('Error' + errorResponse);
|
alert('Error' + errorResponse);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function getRandomHex(size: number) {
|
||||||
|
const bytes = new Uint8Array(size);
|
||||||
|
window.crypto.getRandomValues(bytes);
|
||||||
|
return Array.from(bytes)
|
||||||
|
.map((byte) => byte.toString(16).padStart(2, '0'))
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function githubLogin() {
|
||||||
|
const client_id = config.public.GITHUB_CLIENT_ID;
|
||||||
|
const redirect_uri = window.location.origin + '/api';
|
||||||
|
console.log({ redirect_uri })
|
||||||
|
const state = getRandomHex(16);
|
||||||
|
localStorage.setItem("latestCSRFToken", state);
|
||||||
|
const link = `https://github.com/login/oauth/authorize?client_id=${client_id}&response_type=code&scope=repo&redirect_uri=${redirect_uri}/integrations/github/oauth2/callback&state=${state}`;
|
||||||
|
window.location.assign(link);
|
||||||
|
}
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (route.query.github_access_token) {
|
||||||
|
//TODO: Something
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
@@ -103,23 +130,34 @@ function handleOnError(errorResponse: any) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-text/80 text-[1.2rem] text-center w-[70%] poppins mt-2">
|
<div class="text-text/80 text-[1.2rem] text-center w-[70%] poppins mt-2">
|
||||||
Real-time analytics for 15+ JS/TS frameworks
|
Track web analytics and custom events
|
||||||
<br>
|
<br>
|
||||||
with one-line code setup.
|
with extreme simplicity in under 30 sec.
|
||||||
<br>
|
<br>
|
||||||
<div class="font-bold poppins mt-4">
|
<!-- <div class="font-bold poppins mt-4">
|
||||||
Start for Free now! Up to 3k visits/events monthly.
|
Start for Free now! Up to 3k visits/events monthly.
|
||||||
</div>
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-12">
|
<div class="mt-12">
|
||||||
|
|
||||||
<div v-if="!isNoAuth" @click="login"
|
|
||||||
class="hover:bg-accent cursor-pointer flex text-[1.3rem] gap-4 items-center border-[1px] border-gray-400 rounded-lg px-8 py-3 relative z-[2]">
|
<div v-if="!isNoAuth" class="flex flex-col gap-2">
|
||||||
<div class="flex items-center">
|
<div @click="login"
|
||||||
<i class="fab fa-google"></i>
|
class="hover:bg-accent cursor-pointer flex text-[1.3rem] gap-4 items-center border-[1px] border-gray-400 rounded-lg px-8 py-3 relative z-[2]">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fab fa-google"></i>
|
||||||
|
</div>
|
||||||
|
Continue with Google
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class=" opacity-35 cursor-not-allowed flex text-[1.3rem] gap-4 items-center border-[1px] border-gray-400 rounded-lg px-8 py-3 relative z-[2]">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fab fa-github"></i>
|
||||||
|
</div>
|
||||||
|
Continue with GitHub
|
||||||
</div>
|
</div>
|
||||||
Continue with Google
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="isNoAuth" @click="loginWithoutAuth"
|
<div v-if="isNoAuth" @click="loginWithoutAuth"
|
||||||
@@ -133,7 +171,7 @@ function handleOnError(errorResponse: any) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-[.9rem] poppins mt-12 text-text-sub text-center relative z-[2]">
|
<div class="text-[.9rem] poppins mt-12 text-text-sub text-center relative z-[2]">
|
||||||
By continuing you are indicating that you accept
|
By continuing you are accepting
|
||||||
<br>
|
<br>
|
||||||
our
|
our
|
||||||
<a class="underline" href="https://litlyx.com/terms" target="_blank">Terms of Service</a> and
|
<a class="underline" href="https://litlyx.com/terms" target="_blank">Terms of Service</a> and
|
||||||
|
|||||||
308
dashboard/pnpm-lock.yaml
generated
308
dashboard/pnpm-lock.yaml
generated
@@ -17,6 +17,9 @@ importers:
|
|||||||
chart.js:
|
chart.js:
|
||||||
specifier: ^3.9.1
|
specifier: ^3.9.1
|
||||||
version: 3.9.1
|
version: 3.9.1
|
||||||
|
chartjs-plugin-annotation:
|
||||||
|
specifier: ^2.2.1
|
||||||
|
version: 2.2.1(chart.js@3.9.1)
|
||||||
date-fns:
|
date-fns:
|
||||||
specifier: ^3.6.0
|
specifier: ^3.6.0
|
||||||
version: 3.6.0
|
version: 3.6.0
|
||||||
@@ -45,8 +48,8 @@ importers:
|
|||||||
specifier: ^0.0.11
|
specifier: ^0.0.11
|
||||||
version: 0.0.11(@types/node@20.12.12)(rollup@4.18.0)(typescript@5.4.2)(vite@5.2.12(@types/node@20.12.12)(sass@1.77.2)(terser@5.31.0))(vue@3.4.27(typescript@5.4.2))
|
version: 0.0.11(@types/node@20.12.12)(rollup@4.18.0)(typescript@5.4.2)(vite@5.2.12(@types/node@20.12.12)(sass@1.77.2)(terser@5.31.0))(vue@3.4.27(typescript@5.4.2))
|
||||||
openai:
|
openai:
|
||||||
specifier: ^4.47.1
|
specifier: ^4.61.0
|
||||||
version: 4.47.2(encoding@0.1.13)
|
version: 4.61.0(encoding@0.1.13)
|
||||||
pdfkit:
|
pdfkit:
|
||||||
specifier: ^0.15.0
|
specifier: ^0.15.0
|
||||||
version: 0.15.0
|
version: 0.15.0
|
||||||
@@ -71,9 +74,15 @@ importers:
|
|||||||
vue-chart-3:
|
vue-chart-3:
|
||||||
specifier: ^3.1.8
|
specifier: ^3.1.8
|
||||||
version: 3.1.8(chart.js@3.9.1)(vue@3.4.27(typescript@5.4.2))
|
version: 3.1.8(chart.js@3.9.1)(vue@3.4.27(typescript@5.4.2))
|
||||||
|
vue-markdown-render:
|
||||||
|
specifier: ^2.2.1
|
||||||
|
version: 2.2.1(vue@3.4.27(typescript@5.4.2))
|
||||||
vue-router:
|
vue-router:
|
||||||
specifier: ^4.3.0
|
specifier: ^4.3.0
|
||||||
version: 4.3.2(vue@3.4.27(typescript@5.4.2))
|
version: 4.3.2(vue@3.4.27(typescript@5.4.2))
|
||||||
|
winston:
|
||||||
|
specifier: ^3.14.2
|
||||||
|
version: 3.14.2
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@nuxt/ui':
|
'@nuxt/ui':
|
||||||
specifier: ^2.15.2
|
specifier: ^2.15.2
|
||||||
@@ -81,6 +90,9 @@ importers:
|
|||||||
'@types/jsonwebtoken':
|
'@types/jsonwebtoken':
|
||||||
specifier: ^9.0.6
|
specifier: ^9.0.6
|
||||||
version: 9.0.6
|
version: 9.0.6
|
||||||
|
'@types/markdown-it':
|
||||||
|
specifier: ^14.1.2
|
||||||
|
version: 14.1.2
|
||||||
'@types/nodemailer':
|
'@types/nodemailer':
|
||||||
specifier: ^6.4.15
|
specifier: ^6.4.15
|
||||||
version: 6.4.15
|
version: 6.4.15
|
||||||
@@ -304,6 +316,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-EeEjMobfuJrwoctj7FA1y1KEbM0+Q1xSjobIEyie9k4haVEBB7vkDvsasw1pM3rO39mL2akxIAzLMUAtrMHZhA==}
|
resolution: {integrity: sha512-EeEjMobfuJrwoctj7FA1y1KEbM0+Q1xSjobIEyie9k4haVEBB7vkDvsasw1pM3rO39mL2akxIAzLMUAtrMHZhA==}
|
||||||
engines: {node: '>=16.13'}
|
engines: {node: '>=16.13'}
|
||||||
|
|
||||||
|
'@colors/colors@1.6.0':
|
||||||
|
resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==}
|
||||||
|
engines: {node: '>=0.1.90'}
|
||||||
|
|
||||||
'@csstools/selector-resolve-nested@1.1.0':
|
'@csstools/selector-resolve-nested@1.1.0':
|
||||||
resolution: {integrity: sha512-uWvSaeRcHyeNenKg8tp17EVDRkpflmdyvbE0DHo6D/GdBb6PDnCYYU6gRpXhtICMGMcahQmj2zGxwFM/WC8hCg==}
|
resolution: {integrity: sha512-uWvSaeRcHyeNenKg8tp17EVDRkpflmdyvbE0DHo6D/GdBb6PDnCYYU6gRpXhtICMGMcahQmj2zGxwFM/WC8hCg==}
|
||||||
engines: {node: ^14 || ^16 || >=18}
|
engines: {node: ^14 || ^16 || >=18}
|
||||||
@@ -316,6 +332,9 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
postcss-selector-parser: ^6.0.13
|
postcss-selector-parser: ^6.0.13
|
||||||
|
|
||||||
|
'@dabh/diagnostics@2.0.3':
|
||||||
|
resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==}
|
||||||
|
|
||||||
'@egoist/tailwindcss-icons@1.8.0':
|
'@egoist/tailwindcss-icons@1.8.0':
|
||||||
resolution: {integrity: sha512-75LfllKL2lq0sGH+wcpsn/sLtJ0kMkDWmcZTLAB76QLDTmfsFu4QHwZVbtCD2woGyKl9c8KvtOUW9JSjRqOVtA==}
|
resolution: {integrity: sha512-75LfllKL2lq0sGH+wcpsn/sLtJ0kMkDWmcZTLAB76QLDTmfsFu4QHwZVbtCD2woGyKl9c8KvtOUW9JSjRqOVtA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1168,9 +1187,18 @@ packages:
|
|||||||
'@types/jsonwebtoken@9.0.6':
|
'@types/jsonwebtoken@9.0.6':
|
||||||
resolution: {integrity: sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==}
|
resolution: {integrity: sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==}
|
||||||
|
|
||||||
|
'@types/linkify-it@5.0.0':
|
||||||
|
resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
|
||||||
|
|
||||||
'@types/lodash@4.17.7':
|
'@types/lodash@4.17.7':
|
||||||
resolution: {integrity: sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==}
|
resolution: {integrity: sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==}
|
||||||
|
|
||||||
|
'@types/markdown-it@14.1.2':
|
||||||
|
resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==}
|
||||||
|
|
||||||
|
'@types/mdurl@2.0.0':
|
||||||
|
resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==}
|
||||||
|
|
||||||
'@types/node-fetch@2.6.11':
|
'@types/node-fetch@2.6.11':
|
||||||
resolution: {integrity: sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==}
|
resolution: {integrity: sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==}
|
||||||
|
|
||||||
@@ -1186,12 +1214,18 @@ packages:
|
|||||||
'@types/pdfkit@0.13.4':
|
'@types/pdfkit@0.13.4':
|
||||||
resolution: {integrity: sha512-ixGNDHYJCCKuamY305wbfYSphZ2WPe8FPkjn8oF4fHV+PgPV4V+hecPh2VOS2h4RNtpSB3zQcR4sCpNvvrEb1A==}
|
resolution: {integrity: sha512-ixGNDHYJCCKuamY305wbfYSphZ2WPe8FPkjn8oF4fHV+PgPV4V+hecPh2VOS2h4RNtpSB3zQcR4sCpNvvrEb1A==}
|
||||||
|
|
||||||
|
'@types/qs@6.9.16':
|
||||||
|
resolution: {integrity: sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==}
|
||||||
|
|
||||||
'@types/resize-observer-browser@0.1.11':
|
'@types/resize-observer-browser@0.1.11':
|
||||||
resolution: {integrity: sha512-cNw5iH8JkMkb3QkCoe7DaZiawbDQEUX8t7iuQaRTyLOyQCR2h+ibBD4GJt7p5yhUHrlOeL7ZtbxNHeipqNsBzQ==}
|
resolution: {integrity: sha512-cNw5iH8JkMkb3QkCoe7DaZiawbDQEUX8t7iuQaRTyLOyQCR2h+ibBD4GJt7p5yhUHrlOeL7ZtbxNHeipqNsBzQ==}
|
||||||
|
|
||||||
'@types/resolve@1.20.2':
|
'@types/resolve@1.20.2':
|
||||||
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
|
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
|
||||||
|
|
||||||
|
'@types/triple-beam@1.3.5':
|
||||||
|
resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==}
|
||||||
|
|
||||||
'@types/web-bluetooth@0.0.20':
|
'@types/web-bluetooth@0.0.20':
|
||||||
resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==}
|
resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==}
|
||||||
|
|
||||||
@@ -1425,20 +1459,20 @@ packages:
|
|||||||
'@vue/reactivity@3.4.27':
|
'@vue/reactivity@3.4.27':
|
||||||
resolution: {integrity: sha512-kK0g4NknW6JX2yySLpsm2jlunZJl2/RJGZ0H9ddHdfBVHcNzxmQ0sS0b09ipmBoQpY8JM2KmUw+a6sO8Zo+zIA==}
|
resolution: {integrity: sha512-kK0g4NknW6JX2yySLpsm2jlunZJl2/RJGZ0H9ddHdfBVHcNzxmQ0sS0b09ipmBoQpY8JM2KmUw+a6sO8Zo+zIA==}
|
||||||
|
|
||||||
'@vue/reactivity@3.4.38':
|
'@vue/reactivity@3.5.6':
|
||||||
resolution: {integrity: sha512-4vl4wMMVniLsSYYeldAKzbk72+D3hUnkw9z8lDeJacTxAkXeDAP1uE9xr2+aKIN0ipOL8EG2GPouVTH6yF7Gnw==}
|
resolution: {integrity: sha512-shZ+KtBoHna5GyUxWfoFVBCVd7k56m6lGhk5e+J9AKjheHF6yob5eukssHRI+rzvHBiU1sWs/1ZhNbLExc5oYQ==}
|
||||||
|
|
||||||
'@vue/runtime-core@3.4.27':
|
'@vue/runtime-core@3.4.27':
|
||||||
resolution: {integrity: sha512-7aYA9GEbOOdviqVvcuweTLe5Za4qBZkUY7SvET6vE8kyypxVgaT1ixHLg4urtOlrApdgcdgHoTZCUuTGap/5WA==}
|
resolution: {integrity: sha512-7aYA9GEbOOdviqVvcuweTLe5Za4qBZkUY7SvET6vE8kyypxVgaT1ixHLg4urtOlrApdgcdgHoTZCUuTGap/5WA==}
|
||||||
|
|
||||||
'@vue/runtime-core@3.4.38':
|
'@vue/runtime-core@3.5.6':
|
||||||
resolution: {integrity: sha512-21z3wA99EABtuf+O3IhdxP0iHgkBs1vuoCAsCKLVJPEjpVqvblwBnTj42vzHRlWDCyxu9ptDm7sI2ZMcWrQqlA==}
|
resolution: {integrity: sha512-FpFULR6+c2lI+m1fIGONLDqPQO34jxV8g6A4wBOgne8eSRHP6PQL27+kWFIx5wNhhjkO7B4rgtsHAmWv7qKvbg==}
|
||||||
|
|
||||||
'@vue/runtime-dom@3.4.27':
|
'@vue/runtime-dom@3.4.27':
|
||||||
resolution: {integrity: sha512-ScOmP70/3NPM+TW9hvVAz6VWWtZJqkbdf7w6ySsws+EsqtHvkhxaWLecrTorFxsawelM5Ys9FnDEMt6BPBDS0Q==}
|
resolution: {integrity: sha512-ScOmP70/3NPM+TW9hvVAz6VWWtZJqkbdf7w6ySsws+EsqtHvkhxaWLecrTorFxsawelM5Ys9FnDEMt6BPBDS0Q==}
|
||||||
|
|
||||||
'@vue/runtime-dom@3.4.38':
|
'@vue/runtime-dom@3.5.6':
|
||||||
resolution: {integrity: sha512-afZzmUreU7vKwKsV17H1NDThEEmdYI+GCAK/KY1U957Ig2NATPVjCROv61R19fjZNzMmiU03n79OMnXyJVN0UA==}
|
resolution: {integrity: sha512-SDPseWre45G38ENH2zXRAHL1dw/rr5qp91lS4lt/nHvMr0MhsbCbihGAWLXNB/6VfFOJe2O+RBRkXU+CJF7/sw==}
|
||||||
|
|
||||||
'@vue/server-renderer@3.4.27':
|
'@vue/server-renderer@3.4.27':
|
||||||
resolution: {integrity: sha512-dlAMEuvmeA3rJsOMJ2J1kXU7o7pOxgsNHVr9K8hB3ImIkSuBrIdy0vF66h8gf8Tuinf1TK3mPAz2+2sqyf3KzA==}
|
resolution: {integrity: sha512-dlAMEuvmeA3rJsOMJ2J1kXU7o7pOxgsNHVr9K8hB3ImIkSuBrIdy0vF66h8gf8Tuinf1TK3mPAz2+2sqyf3KzA==}
|
||||||
@@ -1448,11 +1482,8 @@ packages:
|
|||||||
'@vue/shared@3.4.27':
|
'@vue/shared@3.4.27':
|
||||||
resolution: {integrity: sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==}
|
resolution: {integrity: sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==}
|
||||||
|
|
||||||
'@vue/shared@3.4.34':
|
'@vue/shared@3.5.6':
|
||||||
resolution: {integrity: sha512-x5LmiRLpRsd9KTjAB8MPKf0CDPMcuItjP0gbNqFCIgL1I8iYp4zglhj9w9FPCdIbHG2M91RVeIbArFfFTz9I3A==}
|
resolution: {integrity: sha512-eidH0HInnL39z6wAt6SFIwBrvGOpDWsDxlw3rCgo1B+CQ1781WzQUSU3YjxgdkcJo9Q8S6LmXTkvI+cLHGkQfA==}
|
||||||
|
|
||||||
'@vue/shared@3.4.38':
|
|
||||||
resolution: {integrity: sha512-q0xCiLkuWWQLzVrecPb0RMsNWyxICOjPrcrwxTUEHb1fsnvni4dcuyG7RT/Ie7VPTvnjzIaWzRMUBsrqNj/hhw==}
|
|
||||||
|
|
||||||
'@vueuse/components@10.10.0':
|
'@vueuse/components@10.10.0':
|
||||||
resolution: {integrity: sha512-HiA10NQ9HJAGnju+8ZK4TyA8LIc0a6BnJmVWDa/k+TRhaYCVacSDU04k0BQ2otV+gghUDdwu98upf6TDRXpoeg==}
|
resolution: {integrity: sha512-HiA10NQ9HJAGnju+8ZK4TyA8LIc0a6BnJmVWDa/k+TRhaYCVacSDU04k0BQ2otV+gghUDdwu98upf6TDRXpoeg==}
|
||||||
@@ -1826,6 +1857,11 @@ packages:
|
|||||||
chart.js@3.9.1:
|
chart.js@3.9.1:
|
||||||
resolution: {integrity: sha512-Ro2JbLmvg83gXF5F4sniaQ+lTbSv18E+TIf2cOeiH1Iqd2PGFOtem+DUufMZsCJwFE7ywPOpfXFBwRTGq7dh6w==}
|
resolution: {integrity: sha512-Ro2JbLmvg83gXF5F4sniaQ+lTbSv18E+TIf2cOeiH1Iqd2PGFOtem+DUufMZsCJwFE7ywPOpfXFBwRTGq7dh6w==}
|
||||||
|
|
||||||
|
chartjs-plugin-annotation@2.2.1:
|
||||||
|
resolution: {integrity: sha512-RL9UtrFr2SXd7C47zD0MZqn6ZLgrcRt3ySC6cYal2amBdANcYB1QcwFXcpKWAYnO4SGJYRok7P5rKDDNgJMA/w==}
|
||||||
|
peerDependencies:
|
||||||
|
chart.js: '>=3.7.0'
|
||||||
|
|
||||||
check-error@1.0.3:
|
check-error@1.0.3:
|
||||||
resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==}
|
resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==}
|
||||||
|
|
||||||
@@ -1884,16 +1920,25 @@ packages:
|
|||||||
color-name@1.1.4:
|
color-name@1.1.4:
|
||||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||||
|
|
||||||
|
color-string@1.9.1:
|
||||||
|
resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
|
||||||
|
|
||||||
color-support@1.1.3:
|
color-support@1.1.3:
|
||||||
resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==}
|
resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
color@3.2.1:
|
||||||
|
resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==}
|
||||||
|
|
||||||
colord@2.9.3:
|
colord@2.9.3:
|
||||||
resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==}
|
resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==}
|
||||||
|
|
||||||
colorette@2.0.20:
|
colorette@2.0.20:
|
||||||
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
|
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
|
||||||
|
|
||||||
|
colorspace@1.1.4:
|
||||||
|
resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==}
|
||||||
|
|
||||||
combined-stream@1.0.8:
|
combined-stream@1.0.8:
|
||||||
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@@ -2264,6 +2309,9 @@ packages:
|
|||||||
emoji-regex@9.2.2:
|
emoji-regex@9.2.2:
|
||||||
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
|
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
|
||||||
|
|
||||||
|
enabled@2.0.0:
|
||||||
|
resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==}
|
||||||
|
|
||||||
encodeurl@1.0.2:
|
encodeurl@1.0.2:
|
||||||
resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==}
|
resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@@ -2275,6 +2323,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-4U5pNsuDl0EhuZpq46M5xPslstkviJuhrdobaRDBk2Jy2KO37FDAJl4lb2KlNabxT0m4MTK2UHNrsAcphE8nyw==}
|
resolution: {integrity: sha512-4U5pNsuDl0EhuZpq46M5xPslstkviJuhrdobaRDBk2Jy2KO37FDAJl4lb2KlNabxT0m4MTK2UHNrsAcphE8nyw==}
|
||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
|
|
||||||
|
entities@3.0.1:
|
||||||
|
resolution: {integrity: sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==}
|
||||||
|
engines: {node: '>=0.12'}
|
||||||
|
|
||||||
entities@4.5.0:
|
entities@4.5.0:
|
||||||
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
||||||
engines: {node: '>=0.12'}
|
engines: {node: '>=0.12'}
|
||||||
@@ -2419,6 +2471,9 @@ packages:
|
|||||||
fastq@1.17.1:
|
fastq@1.17.1:
|
||||||
resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==}
|
resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==}
|
||||||
|
|
||||||
|
fecha@4.2.3:
|
||||||
|
resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==}
|
||||||
|
|
||||||
file-entry-cache@6.0.1:
|
file-entry-cache@6.0.1:
|
||||||
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
|
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
|
||||||
engines: {node: ^10.12.0 || >=12.0.0}
|
engines: {node: ^10.12.0 || >=12.0.0}
|
||||||
@@ -2450,6 +2505,9 @@ packages:
|
|||||||
'@nuxt/kit':
|
'@nuxt/kit':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
fn.name@1.1.0:
|
||||||
|
resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==}
|
||||||
|
|
||||||
focus-trap@7.5.4:
|
focus-trap@7.5.4:
|
||||||
resolution: {integrity: sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==}
|
resolution: {integrity: sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==}
|
||||||
|
|
||||||
@@ -2846,6 +2904,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==}
|
resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
is-arrayish@0.3.2:
|
||||||
|
resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
|
||||||
|
|
||||||
is-bigint@1.0.4:
|
is-bigint@1.0.4:
|
||||||
resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==}
|
resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==}
|
||||||
|
|
||||||
@@ -3146,6 +3207,9 @@ packages:
|
|||||||
kolorist@1.8.0:
|
kolorist@1.8.0:
|
||||||
resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==}
|
resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==}
|
||||||
|
|
||||||
|
kuler@2.0.0:
|
||||||
|
resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==}
|
||||||
|
|
||||||
launch-editor@2.6.1:
|
launch-editor@2.6.1:
|
||||||
resolution: {integrity: sha512-eB/uXmFVpY4zezmGp5XtU21kwo7GBbKB+EQ+UZeWtGb9yAM5xt/Evk+lYH3eRNAtId+ej4u7TYPFZ07w4s7rRw==}
|
resolution: {integrity: sha512-eB/uXmFVpY4zezmGp5XtU21kwo7GBbKB+EQ+UZeWtGb9yAM5xt/Evk+lYH3eRNAtId+ej4u7TYPFZ07w4s7rRw==}
|
||||||
|
|
||||||
@@ -3171,6 +3235,9 @@ packages:
|
|||||||
lines-and-columns@1.2.4:
|
lines-and-columns@1.2.4:
|
||||||
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
|
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
|
||||||
|
|
||||||
|
linkify-it@4.0.1:
|
||||||
|
resolution: {integrity: sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==}
|
||||||
|
|
||||||
listhen@1.7.2:
|
listhen@1.7.2:
|
||||||
resolution: {integrity: sha512-7/HamOm5YD9Wb7CFgAZkKgVPA96WwhcTQoqtm2VTZGVbVVn3IWKRBTgrU7cchA3Q8k9iCsG8Osoi9GX4JsGM9g==}
|
resolution: {integrity: sha512-7/HamOm5YD9Wb7CFgAZkKgVPA96WwhcTQoqtm2VTZGVbVVn3IWKRBTgrU7cchA3Q8k9iCsG8Osoi9GX4JsGM9g==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -3241,6 +3308,10 @@ packages:
|
|||||||
lodash@4.17.21:
|
lodash@4.17.21:
|
||||||
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
||||||
|
|
||||||
|
logform@2.6.1:
|
||||||
|
resolution: {integrity: sha512-CdaO738xRapbKIMVn2m4F6KTj4j7ooJ8POVnebSgKo3KBz5axNXRAL7ZdRjIV6NOr2Uf4vjtRkxrFETOioCqSA==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
|
||||||
loupe@2.3.7:
|
loupe@2.3.7:
|
||||||
resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==}
|
resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==}
|
||||||
|
|
||||||
@@ -3273,12 +3344,19 @@ packages:
|
|||||||
resolution: {integrity: sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA==}
|
resolution: {integrity: sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA==}
|
||||||
engines: {node: ^16.14.0 || >=18.0.0}
|
engines: {node: ^16.14.0 || >=18.0.0}
|
||||||
|
|
||||||
|
markdown-it@13.0.2:
|
||||||
|
resolution: {integrity: sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
mdn-data@2.0.28:
|
mdn-data@2.0.28:
|
||||||
resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==}
|
resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==}
|
||||||
|
|
||||||
mdn-data@2.0.30:
|
mdn-data@2.0.30:
|
||||||
resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==}
|
resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==}
|
||||||
|
|
||||||
|
mdurl@1.0.1:
|
||||||
|
resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==}
|
||||||
|
|
||||||
media-typer@0.3.0:
|
media-typer@0.3.0:
|
||||||
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
|
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@@ -3676,6 +3754,9 @@ packages:
|
|||||||
once@1.4.0:
|
once@1.4.0:
|
||||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||||
|
|
||||||
|
one-time@1.0.0:
|
||||||
|
resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==}
|
||||||
|
|
||||||
onetime@5.1.2:
|
onetime@5.1.2:
|
||||||
resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
|
resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -3699,9 +3780,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
|
resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
openai@4.47.2:
|
openai@4.61.0:
|
||||||
resolution: {integrity: sha512-E3Wq9mYdDSLajmcJm9RO/lCegTKrQ7ilAkMbhob4UgGhTjHwIHI+mXNDNPl5+sGIUp2iVUkpoi772FjYa7JlqA==}
|
resolution: {integrity: sha512-xkygRBRLIUumxzKGb1ug05pWmJROQsHkGuj/N6Jiw2dj0dI19JvbFpErSZKmJ/DA+0IvpcugZqCAyk8iLpyM6Q==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
zod: ^3.23.8
|
||||||
|
peerDependenciesMeta:
|
||||||
|
zod:
|
||||||
|
optional: true
|
||||||
|
|
||||||
openapi-typescript@6.7.6:
|
openapi-typescript@6.7.6:
|
||||||
resolution: {integrity: sha512-c/hfooPx+RBIOPM09GSxABOZhYPblDoyaGhqBkD/59vtpN21jEuWKDlM0KYTvqJVlSYjKs0tBcIdeXKChlSPtw==}
|
resolution: {integrity: sha512-c/hfooPx+RBIOPM09GSxABOZhYPblDoyaGhqBkD/59vtpN21jEuWKDlM0KYTvqJVlSYjKs0tBcIdeXKChlSPtw==}
|
||||||
@@ -4253,6 +4339,10 @@ packages:
|
|||||||
safe-buffer@5.2.1:
|
safe-buffer@5.2.1:
|
||||||
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
||||||
|
|
||||||
|
safe-stable-stringify@2.5.0:
|
||||||
|
resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
safer-buffer@2.1.2:
|
safer-buffer@2.1.2:
|
||||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||||
|
|
||||||
@@ -4347,6 +4437,9 @@ packages:
|
|||||||
simple-git@3.24.0:
|
simple-git@3.24.0:
|
||||||
resolution: {integrity: sha512-QqAKee9Twv+3k8IFOFfPB2hnk6as6Y6ACUpwCtQvRYBAes23Wv3SZlHVobAzqcE8gfsisCvPw3HGW3HYM+VYYw==}
|
resolution: {integrity: sha512-QqAKee9Twv+3k8IFOFfPB2hnk6as6Y6ACUpwCtQvRYBAes23Wv3SZlHVobAzqcE8gfsisCvPw3HGW3HYM+VYYw==}
|
||||||
|
|
||||||
|
simple-swizzle@0.2.2:
|
||||||
|
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
|
||||||
|
|
||||||
sirv@2.0.4:
|
sirv@2.0.4:
|
||||||
resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==}
|
resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
@@ -4429,6 +4522,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==}
|
resolution: {integrity: sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==}
|
||||||
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||||
|
|
||||||
|
stack-trace@0.0.10:
|
||||||
|
resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==}
|
||||||
|
|
||||||
stackback@0.0.2:
|
stackback@0.0.2:
|
||||||
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
|
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
|
||||||
|
|
||||||
@@ -4575,6 +4671,9 @@ packages:
|
|||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
text-hex@1.0.0:
|
||||||
|
resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==}
|
||||||
|
|
||||||
text-table@0.2.0:
|
text-table@0.2.0:
|
||||||
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
||||||
|
|
||||||
@@ -4629,6 +4728,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==}
|
resolution: {integrity: sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
|
triple-beam@1.4.1:
|
||||||
|
resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==}
|
||||||
|
engines: {node: '>= 14.0.0'}
|
||||||
|
|
||||||
ts-interface-checker@0.1.13:
|
ts-interface-checker@0.1.13:
|
||||||
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
|
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
|
||||||
|
|
||||||
@@ -4678,6 +4781,9 @@ packages:
|
|||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
uc.micro@1.0.6:
|
||||||
|
resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==}
|
||||||
|
|
||||||
ufo@1.5.3:
|
ufo@1.5.3:
|
||||||
resolution: {integrity: sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==}
|
resolution: {integrity: sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==}
|
||||||
|
|
||||||
@@ -5032,6 +5138,11 @@ packages:
|
|||||||
vue-devtools-stub@0.1.0:
|
vue-devtools-stub@0.1.0:
|
||||||
resolution: {integrity: sha512-RutnB7X8c5hjq39NceArgXg28WZtZpGc3+J16ljMiYnFhKvd8hITxSWQSQ5bvldxMDU6gG5mkxl1MTQLXckVSQ==}
|
resolution: {integrity: sha512-RutnB7X8c5hjq39NceArgXg28WZtZpGc3+J16ljMiYnFhKvd8hITxSWQSQ5bvldxMDU6gG5mkxl1MTQLXckVSQ==}
|
||||||
|
|
||||||
|
vue-markdown-render@2.2.1:
|
||||||
|
resolution: {integrity: sha512-XkYnC0PMdbs6Vy6j/gZXSvCuOS0787Se5COwXlepRqiqPiunyCIeTPQAO2XnB4Yl04EOHXwLx5y6IuszMWSgyQ==}
|
||||||
|
peerDependencies:
|
||||||
|
vue: ^3.3.4
|
||||||
|
|
||||||
vue-observe-visibility@2.0.0-alpha.1:
|
vue-observe-visibility@2.0.0-alpha.1:
|
||||||
resolution: {integrity: sha512-flFbp/gs9pZniXR6fans8smv1kDScJ8RS7rEpMjhVabiKeq7Qz3D9+eGsypncjfIyyU84saU88XZ0zjbD6Gq/g==}
|
resolution: {integrity: sha512-flFbp/gs9pZniXR6fans8smv1kDScJ8RS7rEpMjhVabiKeq7Qz3D9+eGsypncjfIyyU84saU88XZ0zjbD6Gq/g==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -5079,10 +5190,6 @@ packages:
|
|||||||
typescript:
|
typescript:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
web-streams-polyfill@3.3.3:
|
|
||||||
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
|
|
||||||
engines: {node: '>= 8'}
|
|
||||||
|
|
||||||
web-streams-polyfill@4.0.0-beta.3:
|
web-streams-polyfill@4.0.0-beta.3:
|
||||||
resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==}
|
resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==}
|
||||||
engines: {node: '>= 14'}
|
engines: {node: '>= 14'}
|
||||||
@@ -5142,6 +5249,14 @@ packages:
|
|||||||
wide-align@1.1.5:
|
wide-align@1.1.5:
|
||||||
resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==}
|
resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==}
|
||||||
|
|
||||||
|
winston-transport@4.7.1:
|
||||||
|
resolution: {integrity: sha512-wQCXXVgfv/wUPOfb2x0ruxzwkcZfxcktz6JIMUaPLmcNhO4bZTwA/WtDWK74xV3F2dKu8YadrFv0qhwYjVEwhA==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
|
||||||
|
winston@3.14.2:
|
||||||
|
resolution: {integrity: sha512-CO8cdpBB2yqzEf8v895L+GNKYJiEq8eKlHU38af3snQBQ+sdAIUepjMSguOIJC7ICbzm0ZI+Af2If4vIJrtmOg==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
|
||||||
word-wrap@1.2.5:
|
word-wrap@1.2.5:
|
||||||
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -5463,6 +5578,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
mime: 3.0.0
|
mime: 3.0.0
|
||||||
|
|
||||||
|
'@colors/colors@1.6.0': {}
|
||||||
|
|
||||||
'@csstools/selector-resolve-nested@1.1.0(postcss-selector-parser@6.1.0)':
|
'@csstools/selector-resolve-nested@1.1.0(postcss-selector-parser@6.1.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
postcss-selector-parser: 6.1.0
|
postcss-selector-parser: 6.1.0
|
||||||
@@ -5471,6 +5588,12 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
postcss-selector-parser: 6.1.0
|
postcss-selector-parser: 6.1.0
|
||||||
|
|
||||||
|
'@dabh/diagnostics@2.0.3':
|
||||||
|
dependencies:
|
||||||
|
colorspace: 1.1.4
|
||||||
|
enabled: 2.0.0
|
||||||
|
kuler: 2.0.0
|
||||||
|
|
||||||
'@egoist/tailwindcss-icons@1.8.0(tailwindcss@3.4.3)':
|
'@egoist/tailwindcss-icons@1.8.0(tailwindcss@3.4.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@iconify/utils': 2.1.23
|
'@iconify/utils': 2.1.23
|
||||||
@@ -6538,11 +6661,20 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 20.12.12
|
'@types/node': 20.12.12
|
||||||
|
|
||||||
|
'@types/linkify-it@5.0.0': {}
|
||||||
|
|
||||||
'@types/lodash@4.17.7': {}
|
'@types/lodash@4.17.7': {}
|
||||||
|
|
||||||
|
'@types/markdown-it@14.1.2':
|
||||||
|
dependencies:
|
||||||
|
'@types/linkify-it': 5.0.0
|
||||||
|
'@types/mdurl': 2.0.0
|
||||||
|
|
||||||
|
'@types/mdurl@2.0.0': {}
|
||||||
|
|
||||||
'@types/node-fetch@2.6.11':
|
'@types/node-fetch@2.6.11':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 18.19.33
|
'@types/node': 20.12.12
|
||||||
form-data: 4.0.0
|
form-data: 4.0.0
|
||||||
|
|
||||||
'@types/node@18.19.33':
|
'@types/node@18.19.33':
|
||||||
@@ -6561,10 +6693,14 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 20.12.12
|
'@types/node': 20.12.12
|
||||||
|
|
||||||
|
'@types/qs@6.9.16': {}
|
||||||
|
|
||||||
'@types/resize-observer-browser@0.1.11': {}
|
'@types/resize-observer-browser@0.1.11': {}
|
||||||
|
|
||||||
'@types/resolve@1.20.2': {}
|
'@types/resolve@1.20.2': {}
|
||||||
|
|
||||||
|
'@types/triple-beam@1.3.5': {}
|
||||||
|
|
||||||
'@types/web-bluetooth@0.0.20': {}
|
'@types/web-bluetooth@0.0.20': {}
|
||||||
|
|
||||||
'@types/webidl-conversions@7.0.3': {}
|
'@types/webidl-conversions@7.0.3': {}
|
||||||
@@ -6990,7 +7126,7 @@ snapshots:
|
|||||||
'@volar/language-core': 1.11.1
|
'@volar/language-core': 1.11.1
|
||||||
'@volar/source-map': 1.11.1
|
'@volar/source-map': 1.11.1
|
||||||
'@vue/compiler-dom': 3.4.27
|
'@vue/compiler-dom': 3.4.27
|
||||||
'@vue/shared': 3.4.34
|
'@vue/shared': 3.5.6
|
||||||
computeds: 0.0.1
|
computeds: 0.0.1
|
||||||
minimatch: 9.0.4
|
minimatch: 9.0.4
|
||||||
muggle-string: 0.3.1
|
muggle-string: 0.3.1
|
||||||
@@ -7003,19 +7139,19 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@vue/shared': 3.4.27
|
'@vue/shared': 3.4.27
|
||||||
|
|
||||||
'@vue/reactivity@3.4.38':
|
'@vue/reactivity@3.5.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vue/shared': 3.4.38
|
'@vue/shared': 3.5.6
|
||||||
|
|
||||||
'@vue/runtime-core@3.4.27':
|
'@vue/runtime-core@3.4.27':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vue/reactivity': 3.4.27
|
'@vue/reactivity': 3.4.27
|
||||||
'@vue/shared': 3.4.27
|
'@vue/shared': 3.4.27
|
||||||
|
|
||||||
'@vue/runtime-core@3.4.38':
|
'@vue/runtime-core@3.5.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vue/reactivity': 3.4.38
|
'@vue/reactivity': 3.5.6
|
||||||
'@vue/shared': 3.4.38
|
'@vue/shared': 3.5.6
|
||||||
|
|
||||||
'@vue/runtime-dom@3.4.27':
|
'@vue/runtime-dom@3.4.27':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -7023,11 +7159,11 @@ snapshots:
|
|||||||
'@vue/shared': 3.4.27
|
'@vue/shared': 3.4.27
|
||||||
csstype: 3.1.3
|
csstype: 3.1.3
|
||||||
|
|
||||||
'@vue/runtime-dom@3.4.38':
|
'@vue/runtime-dom@3.5.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vue/reactivity': 3.4.38
|
'@vue/reactivity': 3.5.6
|
||||||
'@vue/runtime-core': 3.4.38
|
'@vue/runtime-core': 3.5.6
|
||||||
'@vue/shared': 3.4.38
|
'@vue/shared': 3.5.6
|
||||||
csstype: 3.1.3
|
csstype: 3.1.3
|
||||||
|
|
||||||
'@vue/server-renderer@3.4.27(vue@3.4.27(typescript@5.4.2))':
|
'@vue/server-renderer@3.4.27(vue@3.4.27(typescript@5.4.2))':
|
||||||
@@ -7038,9 +7174,7 @@ snapshots:
|
|||||||
|
|
||||||
'@vue/shared@3.4.27': {}
|
'@vue/shared@3.4.27': {}
|
||||||
|
|
||||||
'@vue/shared@3.4.34': {}
|
'@vue/shared@3.5.6': {}
|
||||||
|
|
||||||
'@vue/shared@3.4.38': {}
|
|
||||||
|
|
||||||
'@vueuse/components@10.10.0(vue@3.4.27(typescript@5.4.2))':
|
'@vueuse/components@10.10.0(vue@3.4.27(typescript@5.4.2))':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -7429,6 +7563,10 @@ snapshots:
|
|||||||
|
|
||||||
chart.js@3.9.1: {}
|
chart.js@3.9.1: {}
|
||||||
|
|
||||||
|
chartjs-plugin-annotation@2.2.1(chart.js@3.9.1):
|
||||||
|
dependencies:
|
||||||
|
chart.js: 3.9.1
|
||||||
|
|
||||||
check-error@1.0.3:
|
check-error@1.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
get-func-name: 2.0.2
|
get-func-name: 2.0.2
|
||||||
@@ -7487,12 +7625,27 @@ snapshots:
|
|||||||
|
|
||||||
color-name@1.1.4: {}
|
color-name@1.1.4: {}
|
||||||
|
|
||||||
|
color-string@1.9.1:
|
||||||
|
dependencies:
|
||||||
|
color-name: 1.1.4
|
||||||
|
simple-swizzle: 0.2.2
|
||||||
|
|
||||||
color-support@1.1.3: {}
|
color-support@1.1.3: {}
|
||||||
|
|
||||||
|
color@3.2.1:
|
||||||
|
dependencies:
|
||||||
|
color-convert: 1.9.3
|
||||||
|
color-string: 1.9.1
|
||||||
|
|
||||||
colord@2.9.3: {}
|
colord@2.9.3: {}
|
||||||
|
|
||||||
colorette@2.0.20: {}
|
colorette@2.0.20: {}
|
||||||
|
|
||||||
|
colorspace@1.1.4:
|
||||||
|
dependencies:
|
||||||
|
color: 3.2.1
|
||||||
|
text-hex: 1.0.0
|
||||||
|
|
||||||
combined-stream@1.0.8:
|
combined-stream@1.0.8:
|
||||||
dependencies:
|
dependencies:
|
||||||
delayed-stream: 1.0.0
|
delayed-stream: 1.0.0
|
||||||
@@ -7815,6 +7968,8 @@ snapshots:
|
|||||||
|
|
||||||
emoji-regex@9.2.2: {}
|
emoji-regex@9.2.2: {}
|
||||||
|
|
||||||
|
enabled@2.0.0: {}
|
||||||
|
|
||||||
encodeurl@1.0.2: {}
|
encodeurl@1.0.2: {}
|
||||||
|
|
||||||
encoding@0.1.13:
|
encoding@0.1.13:
|
||||||
@@ -7827,6 +7982,8 @@ snapshots:
|
|||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
tapable: 2.2.1
|
tapable: 2.2.1
|
||||||
|
|
||||||
|
entities@3.0.1: {}
|
||||||
|
|
||||||
entities@4.5.0: {}
|
entities@4.5.0: {}
|
||||||
|
|
||||||
env-paths@2.2.1: {}
|
env-paths@2.2.1: {}
|
||||||
@@ -8038,6 +8195,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
reusify: 1.0.4
|
reusify: 1.0.4
|
||||||
|
|
||||||
|
fecha@4.2.3: {}
|
||||||
|
|
||||||
file-entry-cache@6.0.1:
|
file-entry-cache@6.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
flat-cache: 3.2.0
|
flat-cache: 3.2.0
|
||||||
@@ -8069,6 +8228,8 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@nuxt/kit': 3.11.2(rollup@4.18.0)
|
'@nuxt/kit': 3.11.2(rollup@4.18.0)
|
||||||
|
|
||||||
|
fn.name@1.1.0: {}
|
||||||
|
|
||||||
focus-trap@7.5.4:
|
focus-trap@7.5.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
tabbable: 6.2.0
|
tabbable: 6.2.0
|
||||||
@@ -8531,6 +8692,8 @@ snapshots:
|
|||||||
call-bind: 1.0.7
|
call-bind: 1.0.7
|
||||||
get-intrinsic: 1.2.4
|
get-intrinsic: 1.2.4
|
||||||
|
|
||||||
|
is-arrayish@0.3.2: {}
|
||||||
|
|
||||||
is-bigint@1.0.4:
|
is-bigint@1.0.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
has-bigints: 1.0.2
|
has-bigints: 1.0.2
|
||||||
@@ -8829,6 +8992,8 @@ snapshots:
|
|||||||
|
|
||||||
kolorist@1.8.0: {}
|
kolorist@1.8.0: {}
|
||||||
|
|
||||||
|
kuler@2.0.0: {}
|
||||||
|
|
||||||
launch-editor@2.6.1:
|
launch-editor@2.6.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
picocolors: 1.0.1
|
picocolors: 1.0.1
|
||||||
@@ -8854,6 +9019,10 @@ snapshots:
|
|||||||
|
|
||||||
lines-and-columns@1.2.4: {}
|
lines-and-columns@1.2.4: {}
|
||||||
|
|
||||||
|
linkify-it@4.0.1:
|
||||||
|
dependencies:
|
||||||
|
uc.micro: 1.0.6
|
||||||
|
|
||||||
listhen@1.7.2:
|
listhen@1.7.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@parcel/watcher': 2.4.1
|
'@parcel/watcher': 2.4.1
|
||||||
@@ -8924,6 +9093,15 @@ snapshots:
|
|||||||
|
|
||||||
lodash@4.17.21: {}
|
lodash@4.17.21: {}
|
||||||
|
|
||||||
|
logform@2.6.1:
|
||||||
|
dependencies:
|
||||||
|
'@colors/colors': 1.6.0
|
||||||
|
'@types/triple-beam': 1.3.5
|
||||||
|
fecha: 4.2.3
|
||||||
|
ms: 2.1.3
|
||||||
|
safe-stable-stringify: 2.5.0
|
||||||
|
triple-beam: 1.4.1
|
||||||
|
|
||||||
loupe@2.3.7:
|
loupe@2.3.7:
|
||||||
dependencies:
|
dependencies:
|
||||||
get-func-name: 2.0.2
|
get-func-name: 2.0.2
|
||||||
@@ -8973,10 +9151,20 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
markdown-it@13.0.2:
|
||||||
|
dependencies:
|
||||||
|
argparse: 2.0.1
|
||||||
|
entities: 3.0.1
|
||||||
|
linkify-it: 4.0.1
|
||||||
|
mdurl: 1.0.1
|
||||||
|
uc.micro: 1.0.6
|
||||||
|
|
||||||
mdn-data@2.0.28: {}
|
mdn-data@2.0.28: {}
|
||||||
|
|
||||||
mdn-data@2.0.30: {}
|
mdn-data@2.0.30: {}
|
||||||
|
|
||||||
|
mdurl@1.0.1: {}
|
||||||
|
|
||||||
media-typer@0.3.0: {}
|
media-typer@0.3.0: {}
|
||||||
|
|
||||||
memory-pager@1.5.0: {}
|
memory-pager@1.5.0: {}
|
||||||
@@ -9550,6 +9738,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
wrappy: 1.0.2
|
wrappy: 1.0.2
|
||||||
|
|
||||||
|
one-time@1.0.0:
|
||||||
|
dependencies:
|
||||||
|
fn.name: 1.1.0
|
||||||
|
|
||||||
onetime@5.1.2:
|
onetime@5.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
mimic-fn: 2.1.0
|
mimic-fn: 2.1.0
|
||||||
@@ -9578,16 +9770,17 @@ snapshots:
|
|||||||
is-docker: 2.2.1
|
is-docker: 2.2.1
|
||||||
is-wsl: 2.2.0
|
is-wsl: 2.2.0
|
||||||
|
|
||||||
openai@4.47.2(encoding@0.1.13):
|
openai@4.61.0(encoding@0.1.13):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 18.19.33
|
'@types/node': 18.19.33
|
||||||
'@types/node-fetch': 2.6.11
|
'@types/node-fetch': 2.6.11
|
||||||
|
'@types/qs': 6.9.16
|
||||||
abort-controller: 3.0.0
|
abort-controller: 3.0.0
|
||||||
agentkeepalive: 4.5.0
|
agentkeepalive: 4.5.0
|
||||||
form-data-encoder: 1.7.2
|
form-data-encoder: 1.7.2
|
||||||
formdata-node: 4.4.1
|
formdata-node: 4.4.1
|
||||||
node-fetch: 2.7.0(encoding@0.1.13)
|
node-fetch: 2.7.0(encoding@0.1.13)
|
||||||
web-streams-polyfill: 3.3.3
|
qs: 6.12.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- encoding
|
- encoding
|
||||||
|
|
||||||
@@ -10163,6 +10356,8 @@ snapshots:
|
|||||||
|
|
||||||
safe-buffer@5.2.1: {}
|
safe-buffer@5.2.1: {}
|
||||||
|
|
||||||
|
safe-stable-stringify@2.5.0: {}
|
||||||
|
|
||||||
safer-buffer@2.1.2: {}
|
safer-buffer@2.1.2: {}
|
||||||
|
|
||||||
sass@1.77.2:
|
sass@1.77.2:
|
||||||
@@ -10284,6 +10479,10 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
simple-swizzle@0.2.2:
|
||||||
|
dependencies:
|
||||||
|
is-arrayish: 0.3.2
|
||||||
|
|
||||||
sirv@2.0.4:
|
sirv@2.0.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@polka/url': 1.0.0-next.25
|
'@polka/url': 1.0.0-next.25
|
||||||
@@ -10366,6 +10565,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
minipass: 7.1.2
|
minipass: 7.1.2
|
||||||
|
|
||||||
|
stack-trace@0.0.10: {}
|
||||||
|
|
||||||
stackback@0.0.2: {}
|
stackback@0.0.2: {}
|
||||||
|
|
||||||
standard-as-callback@2.1.0: {}
|
standard-as-callback@2.1.0: {}
|
||||||
@@ -10549,6 +10750,8 @@ snapshots:
|
|||||||
commander: 2.20.3
|
commander: 2.20.3
|
||||||
source-map-support: 0.5.21
|
source-map-support: 0.5.21
|
||||||
|
|
||||||
|
text-hex@1.0.0: {}
|
||||||
|
|
||||||
text-table@0.2.0: {}
|
text-table@0.2.0: {}
|
||||||
|
|
||||||
thenify-all@1.6.0:
|
thenify-all@1.6.0:
|
||||||
@@ -10590,6 +10793,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
punycode: 2.3.1
|
punycode: 2.3.1
|
||||||
|
|
||||||
|
triple-beam@1.4.1: {}
|
||||||
|
|
||||||
ts-interface-checker@0.1.13: {}
|
ts-interface-checker@0.1.13: {}
|
||||||
|
|
||||||
tslib@2.6.2: {}
|
tslib@2.6.2: {}
|
||||||
@@ -10629,6 +10834,8 @@ snapshots:
|
|||||||
|
|
||||||
typescript@5.4.2: {}
|
typescript@5.4.2: {}
|
||||||
|
|
||||||
|
uc.micro@1.0.6: {}
|
||||||
|
|
||||||
ufo@1.5.3: {}
|
ufo@1.5.3: {}
|
||||||
|
|
||||||
ultrahtml@1.5.3: {}
|
ultrahtml@1.5.3: {}
|
||||||
@@ -11028,8 +11235,8 @@ snapshots:
|
|||||||
|
|
||||||
vue-chart-3@3.1.8(chart.js@3.9.1)(vue@3.4.27(typescript@5.4.2)):
|
vue-chart-3@3.1.8(chart.js@3.9.1)(vue@3.4.27(typescript@5.4.2)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vue/runtime-core': 3.4.38
|
'@vue/runtime-core': 3.5.6
|
||||||
'@vue/runtime-dom': 3.4.38
|
'@vue/runtime-dom': 3.5.6
|
||||||
chart.js: 3.9.1
|
chart.js: 3.9.1
|
||||||
csstype: 3.1.3
|
csstype: 3.1.3
|
||||||
lodash-es: 4.17.21
|
lodash-es: 4.17.21
|
||||||
@@ -11041,6 +11248,11 @@ snapshots:
|
|||||||
|
|
||||||
vue-devtools-stub@0.1.0: {}
|
vue-devtools-stub@0.1.0: {}
|
||||||
|
|
||||||
|
vue-markdown-render@2.2.1(vue@3.4.27(typescript@5.4.2)):
|
||||||
|
dependencies:
|
||||||
|
markdown-it: 13.0.2
|
||||||
|
vue: 3.4.27(typescript@5.4.2)
|
||||||
|
|
||||||
vue-observe-visibility@2.0.0-alpha.1(vue@3.4.27(typescript@5.4.2)):
|
vue-observe-visibility@2.0.0-alpha.1(vue@3.4.27(typescript@5.4.2)):
|
||||||
dependencies:
|
dependencies:
|
||||||
vue: 3.4.27(typescript@5.4.2)
|
vue: 3.4.27(typescript@5.4.2)
|
||||||
@@ -11098,8 +11310,6 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.4.2
|
typescript: 5.4.2
|
||||||
|
|
||||||
web-streams-polyfill@3.3.3: {}
|
|
||||||
|
|
||||||
web-streams-polyfill@4.0.0-beta.3: {}
|
web-streams-polyfill@4.0.0-beta.3: {}
|
||||||
|
|
||||||
webidl-conversions@3.0.1: {}
|
webidl-conversions@3.0.1: {}
|
||||||
@@ -11164,6 +11374,26 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
string-width: 4.2.3
|
string-width: 4.2.3
|
||||||
|
|
||||||
|
winston-transport@4.7.1:
|
||||||
|
dependencies:
|
||||||
|
logform: 2.6.1
|
||||||
|
readable-stream: 3.6.2
|
||||||
|
triple-beam: 1.4.1
|
||||||
|
|
||||||
|
winston@3.14.2:
|
||||||
|
dependencies:
|
||||||
|
'@colors/colors': 1.6.0
|
||||||
|
'@dabh/diagnostics': 2.0.3
|
||||||
|
async: 3.2.5
|
||||||
|
is-stream: 2.0.1
|
||||||
|
logform: 2.6.1
|
||||||
|
one-time: 1.0.0
|
||||||
|
readable-stream: 3.6.2
|
||||||
|
safe-stable-stringify: 2.5.0
|
||||||
|
stack-trace: 0.0.10
|
||||||
|
triple-beam: 1.4.1
|
||||||
|
winston-transport: 4.7.1
|
||||||
|
|
||||||
word-wrap@1.2.5: {}
|
word-wrap@1.2.5: {}
|
||||||
|
|
||||||
wrap-ansi@7.0.0:
|
wrap-ansi@7.0.0:
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export async function getUserProjectFromId(project_id: string, user: AuthContext
|
|||||||
return project;
|
return project;
|
||||||
} else {
|
} else {
|
||||||
if (!user?.logged) return;
|
if (!user?.logged) return;
|
||||||
|
if (!project_id) return;
|
||||||
const project = await ProjectModel.findById(project_id);
|
const project = await ProjectModel.findById(project_id);
|
||||||
if (!project) return;
|
if (!project) return;
|
||||||
const [hasAccess, role] = await hasAccessToProject(user.id, project_id, project);
|
const [hasAccess, role] = await hasAccessToProject(user.id, project_id, project);
|
||||||
|
|||||||
46
dashboard/server/Logger.ts
Normal file
46
dashboard/server/Logger.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
|
||||||
|
import winston from 'winston';
|
||||||
|
|
||||||
|
const { combine, timestamp, json, errors } = winston.format;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const logger = winston.createLogger({
|
||||||
|
format: combine(
|
||||||
|
errors({ stack: true }),
|
||||||
|
timestamp({
|
||||||
|
format: 'DD-MM-YYYY hh:mm:ss'
|
||||||
|
}),
|
||||||
|
json()
|
||||||
|
),
|
||||||
|
exceptionHandlers: [
|
||||||
|
new winston.transports.File({ filename: 'winston-logs.ndjson' }),
|
||||||
|
new winston.transports.File({ filename: 'winston-exceptions.ndjson' }),
|
||||||
|
],
|
||||||
|
rejectionHandlers: [
|
||||||
|
new winston.transports.File({ filename: 'winston-logs.ndjson' }),
|
||||||
|
new winston.transports.File({ filename: 'winston-rejections.ndjson' }),
|
||||||
|
],
|
||||||
|
transports: [
|
||||||
|
new winston.transports.Console({
|
||||||
|
level: 'debug',
|
||||||
|
format: combine(
|
||||||
|
winston.format.colorize({ all: true }),
|
||||||
|
errors({ stack: true }),
|
||||||
|
timestamp({ format: 'DD-MM-YYYY hh:mm:ss' }),
|
||||||
|
winston.format.printf((info) => {
|
||||||
|
if (info instanceof Error) {
|
||||||
|
return `${info.timestamp} [${info.level}]: ${info.message}\n${info.stack}`;
|
||||||
|
} else {
|
||||||
|
return `${info.timestamp} [${info.level}]: ${info.message}`;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
new winston.transports.File({ filename: 'winston-logs.ndjson' }),
|
||||||
|
new winston.transports.File({
|
||||||
|
level: 'debug',
|
||||||
|
filename: 'winston-debug.ndjson'
|
||||||
|
})
|
||||||
|
]
|
||||||
|
});
|
||||||
30
dashboard/server/ai/Plugin.ts
Normal file
30
dashboard/server/ai/Plugin.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
|
||||||
|
import type OpenAI from 'openai'
|
||||||
|
|
||||||
|
|
||||||
|
export type AIPlugin_TTool<T extends string> = (OpenAI.Chat.Completions.ChatCompletionTool & { function: { name: T } });
|
||||||
|
export type AIPlugin_TFunction<T extends string> = (...args: any[]) => any;
|
||||||
|
|
||||||
|
type AIPlugin_Constructor<Items extends string[]> = {
|
||||||
|
[Key in Items[number]]: {
|
||||||
|
tool: AIPlugin_TTool<Key>,
|
||||||
|
handler: AIPlugin_TFunction<Key>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class AIPlugin<Items extends string[] = []> {
|
||||||
|
constructor(public functions: AIPlugin_Constructor<Items>) { }
|
||||||
|
|
||||||
|
getTools() {
|
||||||
|
const keys = Object.keys(this.functions) as Items;
|
||||||
|
return keys.map((key: Items[number]) => { return this.functions[key].tool });
|
||||||
|
}
|
||||||
|
getHandlers() {
|
||||||
|
const keys = Object.keys(this.functions) as Items;
|
||||||
|
const result: Record<string, any> = {};
|
||||||
|
keys.forEach((key: Items[number]) => {
|
||||||
|
result[key] = this.functions[key].handler;
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
67
dashboard/server/ai/functions/AI_ComposableChart.ts
Normal file
67
dashboard/server/ai/functions/AI_ComposableChart.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
|
||||||
|
import { AIPlugin } from "../Plugin";
|
||||||
|
|
||||||
|
export class AiComposableChart extends AIPlugin<['createComposableChart']> {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
'createComposableChart': {
|
||||||
|
handler: (data: { labels: string, points: number[] }) => {
|
||||||
|
return { ok: true };
|
||||||
|
},
|
||||||
|
tool: {
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: 'createComposableChart',
|
||||||
|
description: 'Creates a chart based on the provided datasets',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
labels: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'string' },
|
||||||
|
description: 'Labels for each data point in the chart'
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Title of the chart to let user understand what is displaying, not include dates'
|
||||||
|
},
|
||||||
|
datasets: {
|
||||||
|
type: 'array',
|
||||||
|
description: 'List of datasets',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
chartType: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['line', 'bar'],
|
||||||
|
description: 'The type of chart to display the dataset, either "line" or "bar"'
|
||||||
|
},
|
||||||
|
points: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'number' },
|
||||||
|
description: 'Numerical values for each data point in the chart'
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Color used to represent the dataset in format "#RRGGBB"'
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Name of the dataset'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['points', 'color', 'chartType', 'name'],
|
||||||
|
description: 'Data points and style information for the dataset'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['labels', 'datasets', 'title']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AiComposableChartInstance = new AiComposableChart();
|
||||||
87
dashboard/server/ai/functions/AI_Events.ts
Normal file
87
dashboard/server/ai/functions/AI_Events.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { EventModel } from "@schema/metrics/EventSchema";
|
||||||
|
import { AdvancedTimelineAggregationOptions, executeAdvancedTimelineAggregation, executeTimelineAggregation, fillAndMergeTimelineAggregationV2 } from "~/server/services/TimelineService";
|
||||||
|
import { Types } from "mongoose";
|
||||||
|
import { AIPlugin, AIPlugin_TTool } from "../Plugin";
|
||||||
|
|
||||||
|
|
||||||
|
const getEventsCountTool: AIPlugin_TTool<'getEventsCount'> = {
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: 'getEventsCount',
|
||||||
|
description: 'Gets the number of events received on a date range, can also specify the event name and the metadata associated',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
from: { type: 'string', description: 'ISO string of start date including hours' },
|
||||||
|
to: { type: 'string', description: 'ISO string of end date including hours' },
|
||||||
|
name: { type: 'string', description: 'Name of the events to get' },
|
||||||
|
metadata: { type: 'object', description: 'Metadata of events to get' },
|
||||||
|
},
|
||||||
|
required: ['from', 'to']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getEventsTimelineTool: AIPlugin_TTool<'getEventsTimeline'> = {
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: 'getEventsTimeline',
|
||||||
|
description: 'Gets an array of date and count for events received on a date range. Should be used to create charts.',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
from: { type: 'string', description: 'ISO string of start date including hours' },
|
||||||
|
to: { type: 'string', description: 'ISO string of end date including hours' },
|
||||||
|
name: { type: 'string', description: 'Name of the events to get' },
|
||||||
|
metadata: { type: 'object', description: 'Metadata of events to get' },
|
||||||
|
},
|
||||||
|
required: ['from', 'to']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AiEvents extends AIPlugin<['getEventsCount', 'getEventsTimeline']> {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
|
||||||
|
super({
|
||||||
|
'getEventsCount': {
|
||||||
|
handler: async (data: { project_id: string, from?: string, to?: string, name?: string, metadata?: string }) => {
|
||||||
|
const query: any = {
|
||||||
|
project_id: data.project_id,
|
||||||
|
created_at: {
|
||||||
|
$gt: data.from ? new Date(data.from).getTime() : new Date(2023).getTime(),
|
||||||
|
$lt: data.to ? new Date(data.to).getTime() : new Date().getTime(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (data.metadata) query.metadata = data.metadata;
|
||||||
|
if (data.name) query.name = data.name;
|
||||||
|
const result = await EventModel.countDocuments(query);
|
||||||
|
return { count: result };
|
||||||
|
},
|
||||||
|
tool: getEventsCountTool
|
||||||
|
},
|
||||||
|
'getEventsTimeline': {
|
||||||
|
handler: async (data: { project_id: string, from: string, to: string, name?: string, metadata?: string }) => {
|
||||||
|
const query: AdvancedTimelineAggregationOptions & { customMatch: Record<string, any> } = {
|
||||||
|
projectId: new Types.ObjectId(data.project_id) as any,
|
||||||
|
model: EventModel,
|
||||||
|
from: data.from, to: data.to, slice: 'day',
|
||||||
|
customMatch: {}
|
||||||
|
}
|
||||||
|
if (data.metadata) query.customMatch.metadata = data.metadata;
|
||||||
|
if (data.name) query.customMatch.name = data.name;
|
||||||
|
|
||||||
|
const timelineData = await executeAdvancedTimelineAggregation(query);
|
||||||
|
const timelineFilledMerged = fillAndMergeTimelineAggregationV2(timelineData, 'day', data.from, data.to);
|
||||||
|
return { data: timelineFilledMerged };
|
||||||
|
},
|
||||||
|
tool: getEventsTimelineTool
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AiEventsInstance = new AiEvents();
|
||||||
|
|
||||||
87
dashboard/server/ai/functions/AI_Visits.ts
Normal file
87
dashboard/server/ai/functions/AI_Visits.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||||
|
import { AdvancedTimelineAggregationOptions, executeAdvancedTimelineAggregation, executeTimelineAggregation, fillAndMergeTimelineAggregationV2 } from "~/server/services/TimelineService";
|
||||||
|
import { Types } from "mongoose";
|
||||||
|
import { AIPlugin, AIPlugin_TTool } from "../Plugin";
|
||||||
|
|
||||||
|
|
||||||
|
const getVisitsCountsTool: AIPlugin_TTool<'getVisitsCount'> = {
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: 'getVisitsCount',
|
||||||
|
description: 'Gets the number of visits received on a date range',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
from: { type: 'string', description: 'ISO string of start date including hours' },
|
||||||
|
to: { type: 'string', description: 'ISO string of end date including hours' },
|
||||||
|
website: { type: 'string', description: 'The website of the visits' },
|
||||||
|
page: { type: 'string', description: 'The page of the visit' }
|
||||||
|
},
|
||||||
|
required: ['from', 'to']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getVisitsTimelineTool: AIPlugin_TTool<'getVisitsTimeline'> = {
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: 'getVisitsTimeline',
|
||||||
|
description: 'Gets an array of date and count for events received on a date range. Should be used to create charts.',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
from: { type: 'string', description: 'ISO string of start date including hours' },
|
||||||
|
to: { type: 'string', description: 'ISO string of end date including hours' },
|
||||||
|
website: { type: 'string', description: 'The website of the visits' },
|
||||||
|
page: { type: 'string', description: 'The page of the visit' }
|
||||||
|
},
|
||||||
|
required: ['from', 'to']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AiVisits extends AIPlugin<['getVisitsCount', 'getVisitsTimeline']> {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
|
||||||
|
super({
|
||||||
|
'getVisitsCount': {
|
||||||
|
handler: async (data: { project_id: string, from?: string, to?: string, website?: string, page?: string }) => {
|
||||||
|
const query: any = {
|
||||||
|
project_id: data.project_id,
|
||||||
|
created_at: {
|
||||||
|
$gt: data.from ? new Date(data.from).getTime() : new Date(2023).getTime(),
|
||||||
|
$lt: data.to ? new Date(data.to).getTime() : new Date().getTime(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (data.website) query.website = data.website;
|
||||||
|
if (data.page) query.page = data.page;
|
||||||
|
const result = await VisitModel.countDocuments(query);
|
||||||
|
return { count: result };
|
||||||
|
},
|
||||||
|
tool: getVisitsCountsTool
|
||||||
|
},
|
||||||
|
'getVisitsTimeline': {
|
||||||
|
handler: async (data: { project_id: string, from: string, to: string, website?: string, page?: string }) => {
|
||||||
|
const query: AdvancedTimelineAggregationOptions & { customMatch: Record<string, any> } = {
|
||||||
|
projectId: new Types.ObjectId(data.project_id) as any,
|
||||||
|
model: VisitModel,
|
||||||
|
from: data.from, to: data.to, slice: 'day',
|
||||||
|
customMatch: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.website) query.customMatch.website = data.website;
|
||||||
|
if (data.page) query.customMatch.page = data.page;
|
||||||
|
|
||||||
|
const timelineData = await executeAdvancedTimelineAggregation(query);
|
||||||
|
const timelineFilledMerged = fillAndMergeTimelineAggregationV2(timelineData, 'day', data.from, data.to);
|
||||||
|
return { data: timelineFilledMerged };
|
||||||
|
},
|
||||||
|
tool: getVisitsTimelineTool
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AiVisitsInstance = new AiVisits();
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||||
import { AiChatModel } from "@schema/ai/AiChatSchema";
|
import { AiChatModel } from "@schema/ai/AiChatSchema";
|
||||||
import { sendMessageOnChat } from "~/server/services/AiService";
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||||
import { AiChatModel } from "@schema/ai/AiChatSchema";
|
import { AiChatModel } from "@schema/ai/AiChatSchema";
|
||||||
import { sendMessageOnChat } from "~/server/services/AiService";
|
import type OpenAI from "openai";
|
||||||
|
import { getChartsInMessage } from "~/server/services/AiService";
|
||||||
|
|
||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
@@ -19,11 +18,14 @@ export default defineEventHandler(async event => {
|
|||||||
const chat = await AiChatModel.findOne({ _id: chat_id, project_id });
|
const chat = await AiChatModel.findOne({ _id: chat_id, project_id });
|
||||||
if (!chat) return;
|
if (!chat) return;
|
||||||
|
|
||||||
const messages = chat.messages.filter(e => {
|
return (chat.messages as OpenAI.Chat.Completions.ChatCompletionMessageParam[])
|
||||||
return (e.role == 'user' || (e.role == 'assistant' && e.content != undefined))
|
.filter(e => e.role === 'assistant' || e.role === 'user')
|
||||||
}).map(e => {
|
.map(e => {
|
||||||
return { role: e.role, content: e.content }
|
const charts = getChartsInMessage(e);
|
||||||
});
|
const content = e.content;
|
||||||
|
return { role: e.role, content, charts }
|
||||||
return messages;
|
})
|
||||||
|
.filter(e=>{
|
||||||
|
return e.charts.length > 0 || e.content
|
||||||
|
})
|
||||||
});
|
});
|
||||||
@@ -23,5 +23,6 @@ export default defineEventHandler(async event => {
|
|||||||
if (chatsRemaining <= 0) return setResponseStatus(event, 400, 'CHAT_LIMIT_REACHED');
|
if (chatsRemaining <= 0) return setResponseStatus(event, 400, 'CHAT_LIMIT_REACHED');
|
||||||
|
|
||||||
const response = await sendMessageOnChat(text, project._id.toString(), chat_id);
|
const response = await sendMessageOnChat(text, project._id.toString(), chat_id);
|
||||||
return response || 'Error getting response';
|
|
||||||
|
return response;
|
||||||
});
|
});
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
|
|
||||||
import OpenAI from "openai";
|
|
||||||
import { EventModel } from "@schema/metrics/EventSchema";
|
|
||||||
|
|
||||||
|
|
||||||
export const AI_EventsFunctions = {
|
|
||||||
getEventsCount: ({ pid, from, to, name, metadata }: any) => {
|
|
||||||
return getEventsCountForAI(pid, from, to, name, metadata);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const getEventsCountForAIDeclaration: OpenAI.Chat.Completions.ChatCompletionTool = {
|
|
||||||
type: 'function',
|
|
||||||
function: {
|
|
||||||
name: 'getEventsCount',
|
|
||||||
description: 'Gets the number of events received on a date range, can also specify the event name and the metadata associated',
|
|
||||||
parameters: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
from: { type: 'string', description: 'ISO string of start date including hours' },
|
|
||||||
to: { type: 'string', description: 'ISO string of end date including hours' },
|
|
||||||
name: { type: 'string', description: 'Name of the events to get' },
|
|
||||||
metadata: { type: 'object', description: 'Metadata of events to get' },
|
|
||||||
},
|
|
||||||
required: ['from', 'to']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AI_EventsTools: OpenAI.Chat.Completions.ChatCompletionTool[] = [
|
|
||||||
getEventsCountForAIDeclaration
|
|
||||||
]
|
|
||||||
|
|
||||||
export async function getEventsCountForAI(project_id: string, from?: string, to?: string, name?: string, metadata?: string) {
|
|
||||||
|
|
||||||
const query: any = {
|
|
||||||
project_id,
|
|
||||||
created_at: {
|
|
||||||
$gt: from ? new Date(from).getTime() : new Date(2023).getTime(),
|
|
||||||
$lt: to ? new Date(to).getTime() : new Date().getTime(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (metadata) query.metadata = metadata;
|
|
||||||
if (name) query.name = name;
|
|
||||||
|
|
||||||
const result = await EventModel.countDocuments(query);
|
|
||||||
|
|
||||||
return { count: result };
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { VisitModel } from "@schema/metrics/VisitSchema";
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export async function getVisitsCountFromDateRange(project_id: string, from?: string, to?: string) {
|
|
||||||
const result = await VisitModel.countDocuments({
|
|
||||||
project_id,
|
|
||||||
created_at: {
|
|
||||||
$gt: from ? new Date(from).getTime() : new Date(2023).getTime(),
|
|
||||||
$lt: to ? new Date(to).getTime() : new Date().getTime(),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return { count: result };
|
|
||||||
}
|
|
||||||
72
dashboard/server/api/integrations/github/oauth2/callback.ts
Normal file
72
dashboard/server/api/integrations/github/oauth2/callback.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
|
||||||
|
import { createUserJwt } from '~/server/AuthManager';
|
||||||
|
import { UserModel } from '@schema/UserSchema';
|
||||||
|
import EmailService from '@services/EmailService';
|
||||||
|
|
||||||
|
const config = useRuntimeConfig();
|
||||||
|
|
||||||
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
|
const { code } = getQuery(event);
|
||||||
|
console.log('CODE', code);
|
||||||
|
|
||||||
|
const redirect_uri = 'http://127.0.0.1:3000'
|
||||||
|
|
||||||
|
const res = await fetch(`https://github.com/login/oauth/access_token?client_id=${config.GITHUB_AUTH_CLIENT_ID}&client_secret=${config.GITHUB_AUTH_CLIENT_SECRET}&code=${code}&redirect_url=${redirect_uri}`, {
|
||||||
|
headers: {
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Accept-Encoding": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
const access_token = data.access_token;
|
||||||
|
|
||||||
|
console.log(data);
|
||||||
|
|
||||||
|
return sendRedirect(event,`http://127.0.0.1:3000/login?github_access_token=${access_token}`)
|
||||||
|
|
||||||
|
|
||||||
|
// const origin = event.headers.get('origin');
|
||||||
|
|
||||||
|
// const tokenResponse = await client.getToken({
|
||||||
|
// code: body.code,
|
||||||
|
// redirect_uri: origin || ''
|
||||||
|
// });
|
||||||
|
|
||||||
|
// const tokens = tokenResponse.tokens;
|
||||||
|
|
||||||
|
// const ticket = await client.verifyIdToken({
|
||||||
|
// idToken: tokens.id_token || '',
|
||||||
|
// audience: GOOGLE_AUTH_CLIENT_ID,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// const payload = ticket.getPayload();
|
||||||
|
// if (!payload) return { error: true, access_token: '' };
|
||||||
|
|
||||||
|
|
||||||
|
// const user = await UserModel.findOne({ email: payload.email });
|
||||||
|
|
||||||
|
// if (user) return { error: false, access_token: createUserJwt({ email: user.email, name: user.name }) }
|
||||||
|
|
||||||
|
|
||||||
|
// const newUser = new UserModel({
|
||||||
|
// email: payload.email,
|
||||||
|
// given_name: payload.given_name,
|
||||||
|
// name: payload.name,
|
||||||
|
// locale: payload.locale,
|
||||||
|
// picture: payload.picture,
|
||||||
|
// created_at: Date.now()
|
||||||
|
// });
|
||||||
|
|
||||||
|
// const savedUser = await newUser.save();
|
||||||
|
|
||||||
|
// setImmediate(() => {
|
||||||
|
// console.log('SENDING WELCOME EMAIL TO', payload.email);
|
||||||
|
// if (payload.email) EmailService.sendWelcomeEmail(payload.email);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// return { error: false, access_token: createUserJwt({ email: savedUser.email, name: savedUser.name }) }
|
||||||
|
|
||||||
|
});
|
||||||
@@ -29,7 +29,7 @@ export default defineEventHandler(async event => {
|
|||||||
|
|
||||||
const project = await ProjectModel.findById(project_id);
|
const project = await ProjectModel.findById(project_id);
|
||||||
if (!project) return setResponseStatus(event, 400, 'Project not found');
|
if (!project) return setResponseStatus(event, 400, 'Project not found');
|
||||||
|
|
||||||
if (project.owner.toString() != userData.id) {
|
if (project.owner.toString() != userData.id) {
|
||||||
return setResponseStatus(event, 400, 'You are not the owner');
|
return setResponseStatus(event, 400, 'You are not the owner');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { EventModel } from "@schema/metrics/EventSchema";
|
|||||||
import { getTimeline } from "./generic";
|
import { getTimeline } from "./generic";
|
||||||
import { Redis, TIMELINE_EXPIRE_TIME } from "~/server/services/CacheService";
|
import { Redis, TIMELINE_EXPIRE_TIME } from "~/server/services/CacheService";
|
||||||
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||||
import { executeTimelineAggregation, fillAndMergeTimelineAggregation } from "~/server/services/TimelineService";
|
import { executeTimelineAggregation, fillAndMergeTimelineAggregation, fillAndMergeTimelineAggregationV2 } from "~/server/services/TimelineService";
|
||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
const project_id = getRequestProjectId(event);
|
const project_id = getRequestProjectId(event);
|
||||||
@@ -27,7 +27,7 @@ export default defineEventHandler(async event => {
|
|||||||
model: EventModel,
|
model: EventModel,
|
||||||
from, to, slice
|
from, to, slice
|
||||||
});
|
});
|
||||||
const timelineFilledMerged = fillAndMergeTimelineAggregation(timelineData, slice);
|
const timelineFilledMerged = fillAndMergeTimelineAggregationV2(timelineData, slice, from, to);
|
||||||
return timelineFilledMerged;
|
return timelineFilledMerged;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { getTimeline } from "./generic";
|
|
||||||
import { VisitModel } from "@schema/metrics/VisitSchema";
|
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||||
import DateService from "@services/DateService";
|
|
||||||
import { Redis, TIMELINE_EXPIRE_TIME } from "~/server/services/CacheService";
|
import { Redis, TIMELINE_EXPIRE_TIME } from "~/server/services/CacheService";
|
||||||
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||||
import { executeAdvancedTimelineAggregation, fillAndMergeTimelineAggregation } from "~/server/services/TimelineService";
|
import { executeAdvancedTimelineAggregation, fillAndMergeTimelineAggregationV2 } from "~/server/services/TimelineService";
|
||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
const project_id = getRequestProjectId(event);
|
const project_id = getRequestProjectId(event);
|
||||||
@@ -31,7 +29,7 @@ export default defineEventHandler(async event => {
|
|||||||
referrer
|
referrer
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const timelineFilledMerged = fillAndMergeTimelineAggregation(timelineData, slice);
|
const timelineFilledMerged = fillAndMergeTimelineAggregationV2(timelineData, slice, from, to);
|
||||||
return timelineFilledMerged;
|
return timelineFilledMerged;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { getTimeline } from "./generic";
|
|||||||
import { Redis, TIMELINE_EXPIRE_TIME } from "~/server/services/CacheService";
|
import { Redis, TIMELINE_EXPIRE_TIME } from "~/server/services/CacheService";
|
||||||
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||||
import { SessionModel } from "@schema/metrics/SessionSchema";
|
import { SessionModel } from "@schema/metrics/SessionSchema";
|
||||||
import { executeTimelineAggregation, fillAndMergeTimelineAggregation } from "~/server/services/TimelineService";
|
import { executeTimelineAggregation, fillAndMergeTimelineAggregationV2 } from "~/server/services/TimelineService";
|
||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
const project_id = getRequestProjectId(event);
|
const project_id = getRequestProjectId(event);
|
||||||
@@ -28,7 +28,7 @@ export default defineEventHandler(async event => {
|
|||||||
model: SessionModel,
|
model: SessionModel,
|
||||||
from, to, slice
|
from, to, slice
|
||||||
});
|
});
|
||||||
const timelineFilledMerged = fillAndMergeTimelineAggregation(timelineData, slice);
|
const timelineFilledMerged = fillAndMergeTimelineAggregationV2(timelineData, slice, from, to);
|
||||||
return timelineFilledMerged;
|
return timelineFilledMerged;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { getTimeline } from "./generic";
|
|||||||
import { Redis, TIMELINE_EXPIRE_TIME } from "~/server/services/CacheService";
|
import { Redis, TIMELINE_EXPIRE_TIME } from "~/server/services/CacheService";
|
||||||
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||||
import { SessionModel } from "@schema/metrics/SessionSchema";
|
import { SessionModel } from "@schema/metrics/SessionSchema";
|
||||||
import { executeAdvancedTimelineAggregation, executeTimelineAggregation, fillAndMergeTimelineAggregation } from "~/server/services/TimelineService";
|
import { executeAdvancedTimelineAggregation, fillAndMergeTimelineAggregationV2 } from "~/server/services/TimelineService";
|
||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
const project_id = getRequestProjectId(event);
|
const project_id = getRequestProjectId(event);
|
||||||
@@ -45,7 +45,7 @@ export default defineEventHandler(async event => {
|
|||||||
count: { $divide: ["$duration", "$count"] }
|
count: { $divide: ["$duration", "$count"] }
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const timelineFilledMerged = fillAndMergeTimelineAggregation(timelineData, slice);
|
const timelineFilledMerged = fillAndMergeTimelineAggregationV2(timelineData, slice, from ,to);
|
||||||
return timelineFilledMerged;
|
return timelineFilledMerged;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { VisitModel } from "@schema/metrics/VisitSchema";
|
|||||||
import { Redis, TIMELINE_EXPIRE_TIME } from "~/server/services/CacheService";
|
import { Redis, TIMELINE_EXPIRE_TIME } from "~/server/services/CacheService";
|
||||||
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||||
import DateService from "@services/DateService";
|
import DateService from "@services/DateService";
|
||||||
import { executeTimelineAggregation, fillAndMergeTimelineAggregation } from "~/server/services/TimelineService";
|
import { executeTimelineAggregation, fillAndMergeTimelineAggregationV2 } from "~/server/services/TimelineService";
|
||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
const project_id = getRequestProjectId(event);
|
const project_id = getRequestProjectId(event);
|
||||||
@@ -28,11 +28,9 @@ export default defineEventHandler(async event => {
|
|||||||
model: VisitModel,
|
model: VisitModel,
|
||||||
from, to, slice,
|
from, to, slice,
|
||||||
});
|
});
|
||||||
const timelineFilledMerged = fillAndMergeTimelineAggregation(timelineData, slice);
|
const timelineFilledMerged = fillAndMergeTimelineAggregationV2(timelineData, slice, from, to);
|
||||||
return timelineFilledMerged;
|
return timelineFilledMerged;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
|
|
||||||
export default defineEventHandler(async event => {
|
|
||||||
|
|
||||||
console.log('TEST');
|
|
||||||
return;
|
|
||||||
});
|
|
||||||
@@ -9,9 +9,13 @@ export default defineEventHandler(async event => {
|
|||||||
if (!project_id) return;
|
if (!project_id) return;
|
||||||
|
|
||||||
const user = getRequestUser(event);
|
const user = getRequestUser(event);
|
||||||
|
if (!user?.logged) return setResponseStatus(event, 400, 'User need to be logged');
|
||||||
|
|
||||||
const project = await getUserProjectFromId(project_id, user);
|
const project = await getUserProjectFromId(project_id, user);
|
||||||
if (!project) return;
|
if (!project) return;
|
||||||
|
|
||||||
|
if (project.owner.toString() != user.id) return setResponseStatus(event, 400, 'You cannot upgrade a project as guest');
|
||||||
|
|
||||||
const body = await readBody(event);
|
const body = await readBody(event);
|
||||||
|
|
||||||
const { planId } = body;
|
const { planId } = body;
|
||||||
|
|||||||
@@ -9,9 +9,13 @@ export default defineEventHandler(async event => {
|
|||||||
if (!project_id) return;
|
if (!project_id) return;
|
||||||
|
|
||||||
const user = getRequestUser(event);
|
const user = getRequestUser(event);
|
||||||
|
if (!user?.logged) return setResponseStatus(event, 400, 'User need to be logged');
|
||||||
|
|
||||||
const project = await getUserProjectFromId(project_id, user);
|
const project = await getUserProjectFromId(project_id, user);
|
||||||
if (!project) return;
|
if (!project) return;
|
||||||
|
|
||||||
|
if (project.owner.toString() != user.id) return setResponseStatus(event, 400, 'You cannot upgrade a project as guest');
|
||||||
|
|
||||||
const body = await readBody(event);
|
const body = await readBody(event);
|
||||||
|
|
||||||
const { planId } = body;
|
const { planId } = body;
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import type Event from 'stripe';
|
|||||||
import { ProjectModel } from '@schema/ProjectSchema';
|
import { ProjectModel } from '@schema/ProjectSchema';
|
||||||
import { PREMIUM_DATA, PREMIUM_PLAN, getPlanFromId, getPlanFromPrice, getPlanFromTag } from '@data/PREMIUM';
|
import { PREMIUM_DATA, PREMIUM_PLAN, getPlanFromId, getPlanFromPrice, getPlanFromTag } from '@data/PREMIUM';
|
||||||
import { ProjectLimitModel } from '@schema/ProjectsLimits';
|
import { ProjectLimitModel } from '@schema/ProjectsLimits';
|
||||||
|
import EmailService from '@services/EmailService'
|
||||||
|
import { UserModel } from '@schema/UserSchema';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -87,6 +89,14 @@ async function onPaymentOnetimeSuccess(event: Event.PaymentIntentSucceededEvent)
|
|||||||
|
|
||||||
await addSubscriptionToProject(project._id.toString(), PLAN, subscription.id, subscription.current_period_start, subscription.current_period_end)
|
await addSubscriptionToProject(project._id.toString(), PLAN, subscription.id, subscription.current_period_start, subscription.current_period_end)
|
||||||
|
|
||||||
|
|
||||||
|
const user = await UserModel.findOne({ _id: project.owner });
|
||||||
|
if (!user) return { ok: false, error: 'USER NOT EXIST FOR PROJECT' + project.id }
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
EmailService.sendPurchaseEmail(user.email, project.name);
|
||||||
|
}, 1);
|
||||||
|
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,6 +135,15 @@ async function onPaymentSuccess(event: Event.InvoicePaidEvent) {
|
|||||||
|
|
||||||
await addSubscriptionToProject(project._id.toString(), PLAN, subscription_id, currentSubscription.current_period_start, currentSubscription.current_period_end)
|
await addSubscriptionToProject(project._id.toString(), PLAN, subscription_id, currentSubscription.current_period_start, currentSubscription.current_period_end)
|
||||||
|
|
||||||
|
const user = await UserModel.findOne({ _id: project.owner });
|
||||||
|
if (!user) return { ok: false, error: 'USER NOT EXIST FOR PROJECT' + project.id }
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (PLAN.ID == 0) return;
|
||||||
|
EmailService.sendPurchaseEmail(user.email, project.name);
|
||||||
|
}, 1);
|
||||||
|
|
||||||
|
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ export default defineEventHandler(async event => {
|
|||||||
const project = await ProjectModel.findById(project_id);
|
const project = await ProjectModel.findById(project_id);
|
||||||
if (!project) return setResponseStatus(event, 400, 'Project not exist');
|
if (!project) return setResponseStatus(event, 400, 'Project not exist');
|
||||||
|
|
||||||
|
if (userData.id != project.owner.toString()) return setResponseStatus(event, 400, 'You cannot delete a project as guest');
|
||||||
|
|
||||||
const projects = await ProjectModel.countDocuments({ owner: userData.id });
|
const projects = await ProjectModel.countDocuments({ owner: userData.id });
|
||||||
if (projects == 1) return setResponseStatus(event, 400, 'Cannot delete last project');
|
if (projects == 1) return setResponseStatus(event, 400, 'Cannot delete last project');
|
||||||
|
|
||||||
|
|||||||
@@ -2,39 +2,47 @@ import mongoose from "mongoose";
|
|||||||
import { Redis } from "~/server/services/CacheService";
|
import { Redis } from "~/server/services/CacheService";
|
||||||
import EmailService from '@services/EmailService';
|
import EmailService from '@services/EmailService';
|
||||||
import StripeService from '~/server/services/StripeService';
|
import StripeService from '~/server/services/StripeService';
|
||||||
|
import { anomalyLoop } from "./services/AnomalyService";
|
||||||
|
import { logger } from "./Logger";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const config = useRuntimeConfig();
|
const config = useRuntimeConfig();
|
||||||
let connection: mongoose.Mongoose;
|
let connection: mongoose.Mongoose;
|
||||||
|
|
||||||
|
|
||||||
export default async () => {
|
export default async () => {
|
||||||
|
|
||||||
console.log('[SERVER] Initializing');
|
logger.info('[SERVER] Initializing');
|
||||||
|
|
||||||
if (config.EMAIL_SERVICE) {
|
if (config.EMAIL_SERVICE) {
|
||||||
EmailService.init(config.BREVO_API_KEY);
|
EmailService.init(config.BREVO_API_KEY);
|
||||||
console.log('[EMAIL] Initialized')
|
logger.info('[EMAIL] Initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (config.STRIPE_SECRET) {
|
if (config.STRIPE_SECRET) {
|
||||||
StripeService.init(config.STRIPE_SECRET, config.STRIPE_WH_SECRET, false);
|
StripeService.init(config.STRIPE_SECRET, config.STRIPE_WH_SECRET, false);
|
||||||
console.log('[STRIPE] Initialized')
|
logger.info('[STRIPE] Initialized');
|
||||||
} else {
|
} else {
|
||||||
StripeService.disable();
|
StripeService.disable();
|
||||||
console.log('[STRIPE] No stripe key - Disabled mode')
|
logger.warn('[STRIPE] No stripe key - Disabled mode');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (!connection || connection.connection.readyState == mongoose.ConnectionStates.disconnected) {
|
if (!connection || connection.connection.readyState == mongoose.ConnectionStates.disconnected) {
|
||||||
console.log('[DATABASE] Connecting');
|
logger.info('[DATABASE] Connecting');
|
||||||
connection = await mongoose.connect(config.MONGO_CONNECTION_STRING);
|
connection = await mongoose.connect(config.MONGO_CONNECTION_STRING);
|
||||||
console.log('[DATABASE] Connected');
|
logger.info('[DATABASE] Connected');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[REDIS] Connecting');
|
logger.info('[REDIS] Connecting');
|
||||||
await Redis.init();
|
await Redis.init();
|
||||||
console.log('[REDIS] Connected');
|
logger.info('[REDIS] Connected');
|
||||||
|
|
||||||
console.log('[SERVER] Completed');
|
logger.info('[SERVER] Completed');
|
||||||
|
|
||||||
|
logger.warn('[ANOMALY LOOP] Disabled');
|
||||||
|
// anomalyLoop();
|
||||||
|
|
||||||
};
|
};
|
||||||
7
dashboard/server/middleware/00-performance-start.ts
Normal file
7
dashboard/server/middleware/00-performance-start.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { logger } from "../Logger"
|
||||||
|
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const start = Date.now();
|
||||||
|
event.context['performance-start'] = start.toString();
|
||||||
|
});
|
||||||
@@ -24,29 +24,21 @@ async function authorizationMiddleware(event: H3Event<EventHandlerRequest>) {
|
|||||||
const authorization = event.headers.get('Authorization');
|
const authorization = event.headers.get('Authorization');
|
||||||
|
|
||||||
if (!authorization) {
|
if (!authorization) {
|
||||||
event.context.auth = { logged: false, }
|
|
||||||
|
event.context.auth = { logged: false }
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
const [type, token] = authorization.split(' ');
|
const [type, token] = authorization.split(' ');
|
||||||
|
|
||||||
const valid = readUserJwt(token);
|
const valid = readUserJwt(token);
|
||||||
|
|
||||||
if (!valid) return event.context.auth = { logged: false }
|
if (!valid) return event.context.auth = { logged: false }
|
||||||
|
|
||||||
const user = await UserModel.findOne({ email: valid.email })
|
const user = await UserModel.findOne({ email: valid.email })
|
||||||
|
|
||||||
if (!user) return event.context.auth = { logged: false };
|
if (!user) return event.context.auth = { logged: false };
|
||||||
|
|
||||||
const premium: any = null;//await PremiumModel.findOne({ user_id: user.id });
|
|
||||||
|
|
||||||
const roles: string[] = [];
|
const roles: string[] = [];
|
||||||
|
|
||||||
if (premium && premium.ends_at.getTime() < Date.now()) {
|
|
||||||
// await PremiumModel.deleteOne({ user_id: user.id });
|
|
||||||
} else if (premium) {
|
|
||||||
roles.push('PREMIUM');
|
|
||||||
roles.push('PREMIUM_' + premium.type);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ADMIN_EMAILS.includes(user.email)) {
|
if (ADMIN_EMAILS.includes(user.email)) {
|
||||||
roles.push('ADMIN');
|
roles.push('ADMIN');
|
||||||
}
|
}
|
||||||
@@ -61,6 +53,7 @@ async function authorizationMiddleware(event: H3Event<EventHandlerRequest>) {
|
|||||||
},
|
},
|
||||||
id: user._id.toString()
|
id: user._id.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
event.context.auth = authContext;
|
event.context.auth = authContext;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
28
dashboard/server/middleware/02-logging.ts
Normal file
28
dashboard/server/middleware/02-logging.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { logger } from "../Logger"
|
||||||
|
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
|
||||||
|
const ip = getRequestAddress(event);
|
||||||
|
const user = getRequestUser(event);
|
||||||
|
|
||||||
|
event.node.res.on('finish', () => {
|
||||||
|
if (!event.context['performance-start']) return;
|
||||||
|
const start = parseInt(event.context['performance-start']);
|
||||||
|
if (isNaN(start)) return;
|
||||||
|
|
||||||
|
const end = Date.now();
|
||||||
|
const duration = (end - start);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
logger.debug('Request without user', { path: event.path, method: event.method, ip, duration });
|
||||||
|
} else if (!user.logged) {
|
||||||
|
logger.debug('Request as guest', { path: event.path, method: event.method, ip, duration });
|
||||||
|
} else {
|
||||||
|
logger.debug(`(${duration}ms) [${event.method}] ${event.path} { ${user.user.email} }`, { ip });
|
||||||
|
}
|
||||||
|
|
||||||
|
// event.node.res.setHeader('X-Total-Response-Time', `${duration.toFixed(2)} ms`);
|
||||||
|
});
|
||||||
|
|
||||||
|
})
|
||||||
@@ -1,59 +1,35 @@
|
|||||||
|
|
||||||
import { getVisitsCountFromDateRange } from '~/server/api/ai/functions/AI_Visits';
|
|
||||||
|
|
||||||
import OpenAI from "openai";
|
import OpenAI from "openai";
|
||||||
import { AiChatModel } from '@schema/ai/AiChatSchema';
|
import { AiChatModel } from '@schema/ai/AiChatSchema';
|
||||||
import { AI_EventsFunctions, AI_EventsTools } from '../api/ai/functions/AI_Events';
|
|
||||||
import { ProjectCountModel } from '@schema/ProjectsCounts';
|
import { ProjectCountModel } from '@schema/ProjectsCounts';
|
||||||
import { ProjectLimitModel } from '@schema/ProjectsLimits';
|
import { ProjectLimitModel } from '@schema/ProjectsLimits';
|
||||||
|
|
||||||
|
import { AiEventsInstance } from '../ai/functions/AI_Events';
|
||||||
|
import { AiVisitsInstance } from '../ai/functions/AI_Visits';
|
||||||
|
import { AiComposableChartInstance } from '../ai/functions/AI_ComposableChart';
|
||||||
|
|
||||||
const { AI_ORG, AI_PROJECT, AI_KEY } = useRuntimeConfig();
|
const { AI_ORG, AI_PROJECT, AI_KEY } = useRuntimeConfig();
|
||||||
|
|
||||||
|
const OPENAI_MODEL: OpenAI.Chat.ChatModel = 'gpt-4o-mini';
|
||||||
|
|
||||||
const openai = new OpenAI({
|
const openai = new OpenAI({
|
||||||
organization: AI_ORG,
|
organization: AI_ORG,
|
||||||
project: AI_PROJECT,
|
project: AI_PROJECT,
|
||||||
apiKey: AI_KEY
|
apiKey: AI_KEY
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// const get_current_date: OpenAI.Chat.Completions.ChatCompletionTool = {
|
|
||||||
// type: 'function',
|
|
||||||
// function: {
|
|
||||||
// name: 'get_current_date',
|
|
||||||
// description: 'Gets the current date as ISO string',
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
const get_visits_count_Schema: OpenAI.Chat.Completions.ChatCompletionTool = {
|
|
||||||
type: 'function',
|
|
||||||
function: {
|
|
||||||
name: 'get_visits_count',
|
|
||||||
description: 'Gets the number of visits received on a date range',
|
|
||||||
parameters: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
from: { type: 'string', description: 'ISO string of start date including hours' },
|
|
||||||
to: { type: 'string', description: 'ISO string of end date including hours' }
|
|
||||||
},
|
|
||||||
required: ['from', 'to']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const tools: OpenAI.Chat.Completions.ChatCompletionTool[] = [
|
const tools: OpenAI.Chat.Completions.ChatCompletionTool[] = [
|
||||||
get_visits_count_Schema,
|
...AiVisitsInstance.getTools(),
|
||||||
...AI_EventsTools
|
...AiEventsInstance.getTools(),
|
||||||
|
...AiComposableChartInstance.getTools()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
const functions: any = {
|
const functions: any = {
|
||||||
get_current_date: async ({ }) => {
|
...AiVisitsInstance.getHandlers(),
|
||||||
return new Date().toISOString();
|
...AiEventsInstance.getHandlers(),
|
||||||
},
|
...AiComposableChartInstance.getHandlers()
|
||||||
get_visits_count: async ({ pid, from, to }: any) => {
|
|
||||||
return await getVisitsCountFromDateRange(pid, from, to);
|
|
||||||
},
|
|
||||||
...AI_EventsFunctions
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -81,6 +57,14 @@ async function setChatTitle(title: string, chat_id?: string) {
|
|||||||
await AiChatModel.updateOne({ _id: chat_id }, { title });
|
await AiChatModel.updateOne({ _id: chat_id }, { title });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function getChartsInMessage(message: OpenAI.Chat.Completions.ChatCompletionMessageParam) {
|
||||||
|
if (message.role != 'assistant') return [];
|
||||||
|
if (!message.tool_calls) return [];
|
||||||
|
if (message.tool_calls.length == 0) return [];
|
||||||
|
return message.tool_calls.filter(e => e.function.name === 'createComposableChart').map(e => e.function.arguments);
|
||||||
|
}
|
||||||
|
|
||||||
export async function sendMessageOnChat(text: string, pid: string, initial_chat_id?: string) {
|
export async function sendMessageOnChat(text: string, pid: string, initial_chat_id?: string) {
|
||||||
|
|
||||||
|
|
||||||
@@ -92,7 +76,8 @@ export async function sendMessageOnChat(text: string, pid: string, initial_chat_
|
|||||||
messages.push(...chatMessages);
|
messages.push(...chatMessages);
|
||||||
} else {
|
} else {
|
||||||
const roleMessage: OpenAI.Chat.Completions.ChatCompletionMessageParam = {
|
const roleMessage: OpenAI.Chat.Completions.ChatCompletionMessageParam = {
|
||||||
role: 'system', content: "Today is " + new Date().toISOString()
|
role: 'system',
|
||||||
|
content: + "Today is " + new Date().toISOString()
|
||||||
}
|
}
|
||||||
messages.push(roleMessage);
|
messages.push(roleMessage);
|
||||||
await addMessageToChat(roleMessage, chat_id);
|
await addMessageToChat(roleMessage, chat_id);
|
||||||
@@ -100,43 +85,36 @@ export async function sendMessageOnChat(text: string, pid: string, initial_chat_
|
|||||||
await setChatTitle(text.substring(0, 110), chat_id);
|
await setChatTitle(text.substring(0, 110), chat_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const userMessage: OpenAI.Chat.Completions.ChatCompletionMessageParam = {
|
const userMessage: OpenAI.Chat.Completions.ChatCompletionMessageParam = { role: 'user', content: text }
|
||||||
role: 'user', content: text
|
|
||||||
}
|
|
||||||
messages.push(userMessage);
|
messages.push(userMessage);
|
||||||
await addMessageToChat(userMessage, chat_id);
|
await addMessageToChat(userMessage, chat_id);
|
||||||
|
|
||||||
let response = await openai.chat.completions.create({ model: 'gpt-3.5-turbo', messages, n: 1, tools });
|
let response = await openai.chat.completions.create({ model: OPENAI_MODEL, messages, n: 1, tools });
|
||||||
|
|
||||||
let responseMessage = response.choices[0].message;
|
const chartsData: string[][] = [];
|
||||||
let toolCalls = responseMessage.tool_calls;
|
|
||||||
|
|
||||||
await addMessageToChat(responseMessage, chat_id);
|
while ((response.choices[0].message.tool_calls?.length || 0) > 0) {
|
||||||
messages.push(responseMessage);
|
await addMessageToChat(response.choices[0].message, chat_id);
|
||||||
|
messages.push(response.choices[0].message);
|
||||||
|
if (response.choices[0].message.tool_calls) {
|
||||||
|
|
||||||
|
console.log('Tools to call', response.choices[0].message.tool_calls.length);
|
||||||
|
chartsData.push(getChartsInMessage(response.choices[0].message));
|
||||||
|
|
||||||
if (toolCalls) {
|
for (const toolCall of response.choices[0].message.tool_calls) {
|
||||||
console.log({ toolCalls: toolCalls.length });
|
const functionName = toolCall.function.name;
|
||||||
for (const toolCall of toolCalls) {
|
console.log('Calling tool function', functionName);
|
||||||
const functionName = toolCall.function.name;
|
const functionToCall = functions[functionName];
|
||||||
const functionToCall = functions[functionName];
|
const functionArgs = JSON.parse(toolCall.function.arguments);
|
||||||
const functionArgs = JSON.parse(toolCall.function.arguments);
|
const functionResponse = await functionToCall({ project_id: pid, ...functionArgs });
|
||||||
console.log('CALLING FUNCTION', functionName, 'WITH PARAMS', functionArgs);
|
messages.push({ tool_call_id: toolCall.id, role: "tool", content: JSON.stringify(functionResponse) });
|
||||||
const functionResponse = await functionToCall({ pid, ...functionArgs });
|
await addMessageToChat({ tool_call_id: toolCall.id, role: "tool", content: JSON.stringify(functionResponse) }, chat_id);
|
||||||
console.log('RESPONSE FUNCTION', functionName, 'WITH VALUE', functionResponse);
|
}
|
||||||
messages.push({ tool_call_id: toolCall.id, role: "tool", content: JSON.stringify(functionResponse) });
|
|
||||||
await addMessageToChat({ tool_call_id: toolCall.id, role: "tool", content: JSON.stringify(functionResponse) }, chat_id);
|
|
||||||
}
|
}
|
||||||
response = await openai.chat.completions.create({ model: 'gpt-4o', messages, n: 1, tools });
|
response = await openai.chat.completions.create({ model: OPENAI_MODEL, messages, n: 1, tools });
|
||||||
responseMessage = response.choices[0].message;
|
|
||||||
toolCalls = responseMessage.tool_calls;
|
|
||||||
|
|
||||||
await addMessageToChat(responseMessage, chat_id);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
await addMessageToChat(response.choices[0].message, chat_id);
|
||||||
await ProjectLimitModel.updateOne({ project_id: pid }, { $inc: { ai_messages: 1 } })
|
await ProjectLimitModel.updateOne({ project_id: pid }, { $inc: { ai_messages: 1 } })
|
||||||
|
return { content: response.choices[0].message.content, charts: chartsData.filter(e => e.length > 0).flat() };
|
||||||
return responseMessage.content;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
152
dashboard/server/services/AnomalyService.ts
Normal file
152
dashboard/server/services/AnomalyService.ts
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import mongoose from "mongoose";
|
||||||
|
import { executeTimelineAggregation } from "./TimelineService";
|
||||||
|
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||||
|
import { AnomalyDomainModel } from '@schema/anomalies/AnomalyDomainSchema';
|
||||||
|
import { AnomalyVisitModel } from '@schema/anomalies/AnomalyVisitSchema';
|
||||||
|
import { AnomalyEventsModel } from '@schema/anomalies/AnomalyEventsSchema';
|
||||||
|
import { EventModel } from "@schema/metrics/EventSchema";
|
||||||
|
import EmailService from "@services/EmailService";
|
||||||
|
|
||||||
|
import * as url from 'url';
|
||||||
|
import { ProjectModel } from "@schema/ProjectSchema";
|
||||||
|
import { UserModel } from "@schema/UserSchema";
|
||||||
|
|
||||||
|
type TAvgInput = { _id: string, count: number }
|
||||||
|
|
||||||
|
const anomalyData = { minutes: 0 }
|
||||||
|
|
||||||
|
async function anomalyCheckAll() {
|
||||||
|
const start = performance.now();
|
||||||
|
console.log('[ANOMALY] START ANOMALY CHECK');
|
||||||
|
const projects = await ProjectModel.find({}, { _id: 1 });
|
||||||
|
for (const project of projects) {
|
||||||
|
await findAnomalies(project.id);
|
||||||
|
}
|
||||||
|
const end = performance.now() - start;
|
||||||
|
console.log('END ANOMALY CHECK', end, 'ms');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function anomalyLoop() {
|
||||||
|
if (anomalyData.minutes == 60 * 12) {
|
||||||
|
anomalyCheckAll();
|
||||||
|
anomalyData.minutes = 0;
|
||||||
|
}
|
||||||
|
anomalyData.minutes++;
|
||||||
|
setTimeout(() => anomalyLoop(), 1000 * 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function movingAverageAnomaly(visits: TAvgInput[], windowSize: number, threshold: number): TAvgInput[] {
|
||||||
|
const anomalies: TAvgInput[] = [];
|
||||||
|
for (let i = windowSize; i < visits.length; i++) {
|
||||||
|
const window = visits.slice(i - windowSize, i);
|
||||||
|
const mean = window.reduce((a, b) => a + b.count, 0) / window.length;
|
||||||
|
const stdDev = Math.sqrt(window.reduce((sum, visit) => sum + Math.pow(visit.count - mean, 2), 0) / window.length);
|
||||||
|
const currentVisit = visits[i];
|
||||||
|
if (Math.abs(currentVisit.count - mean) > threshold * stdDev) {
|
||||||
|
if (currentVisit.count <= mean) continue;
|
||||||
|
anomalies.push(currentVisit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return anomalies;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUrlFromString(str: string) {
|
||||||
|
const res = str.startsWith('http') ? str : 'http://' + str;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function findAnomalies(project_id: string) {
|
||||||
|
|
||||||
|
const THRESHOLD = 6;
|
||||||
|
const WINDOW_SIZE = 14;
|
||||||
|
|
||||||
|
const pid = new mongoose.Types.ObjectId(project_id) as any;
|
||||||
|
|
||||||
|
const from = Date.now() - 1000 * 60 * 60 * 24 * 30;
|
||||||
|
const to = Date.now() - 1000 * 60 * 60 * 24;
|
||||||
|
|
||||||
|
const visitsTimelineData = await executeTimelineAggregation({
|
||||||
|
projectId: pid,
|
||||||
|
model: VisitModel,
|
||||||
|
from, to, slice: 'day'
|
||||||
|
});
|
||||||
|
|
||||||
|
const eventsTimelineData = await executeTimelineAggregation({
|
||||||
|
projectId: pid,
|
||||||
|
model: EventModel,
|
||||||
|
from, to, slice: 'day'
|
||||||
|
});
|
||||||
|
|
||||||
|
const websites: { _id: string, count: number }[] = await VisitModel.aggregate([
|
||||||
|
{ $match: { project_id: pid, created_at: { $gte: new Date(from), $lte: new Date(to) } }, },
|
||||||
|
{ $group: { _id: "$website", count: { $sum: 1, } } }
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
const detectedWebsites: string[] = [];
|
||||||
|
|
||||||
|
if (websites.length > 0) {
|
||||||
|
const rootWebsite = websites.reduce((a, e) => {
|
||||||
|
return a.count > e.count ? a : e;
|
||||||
|
});
|
||||||
|
const rootDomain = new url.URL(getUrlFromString(rootWebsite._id)).hostname;
|
||||||
|
for (const website of websites) {
|
||||||
|
const websiteDomain = new url.URL(getUrlFromString(website._id)).hostname;
|
||||||
|
|
||||||
|
if (websiteDomain === 'localhost') continue;
|
||||||
|
if (websiteDomain === '127.0.0.1') continue;
|
||||||
|
if (websiteDomain === '0.0.0.0') continue;
|
||||||
|
|
||||||
|
if (!websiteDomain.includes(rootDomain)) { detectedWebsites.push(website._id); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const visitAnomalies = movingAverageAnomaly(visitsTimelineData, WINDOW_SIZE, THRESHOLD);
|
||||||
|
const eventAnomalies = movingAverageAnomaly(eventsTimelineData, WINDOW_SIZE, THRESHOLD);
|
||||||
|
|
||||||
|
const shouldSendMail = {
|
||||||
|
visitsEvents: false,
|
||||||
|
domains: false
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const visit of visitAnomalies) {
|
||||||
|
const anomalyAlreadyExist = await AnomalyVisitModel.findOne({ visitDate: visit._id }, { _id: 1 });
|
||||||
|
if (anomalyAlreadyExist) continue;
|
||||||
|
await AnomalyVisitModel.create({ project_id: pid, visitDate: visit._id, created_at: Date.now() });
|
||||||
|
shouldSendMail.visitsEvents = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const event of eventAnomalies) {
|
||||||
|
const anomalyAlreadyExist = await AnomalyEventsModel.findOne({ eventDate: event._id }, { _id: 1 });
|
||||||
|
if (anomalyAlreadyExist) continue;
|
||||||
|
await AnomalyEventsModel.create({ project_id: pid, eventDate: event._id, created_at: Date.now() });
|
||||||
|
shouldSendMail.visitsEvents = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const website of detectedWebsites) {
|
||||||
|
const anomalyAlreadyExist = await AnomalyDomainModel.findOne({ domain: website }, { _id: 1 });
|
||||||
|
if (anomalyAlreadyExist) continue;
|
||||||
|
await AnomalyDomainModel.create({ project_id: pid, domain: website, created_at: Date.now() });
|
||||||
|
shouldSendMail.domains = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const project = await ProjectModel.findById(pid);
|
||||||
|
if (!project) return { ok: false, error: 'Cannot find project with id ' + pid.toString() }
|
||||||
|
const user = await UserModel.findById(project.owner);
|
||||||
|
if (!user) return { ok: false, error: 'Cannot find user with id ' + project.owner.toString() }
|
||||||
|
|
||||||
|
if (shouldSendMail.visitsEvents === true) {
|
||||||
|
await EmailService.sendAnomalyVisitsEventsEmail(user.email, project.name);
|
||||||
|
}
|
||||||
|
if (shouldSendMail.domains === true) {
|
||||||
|
await EmailService.sendAnomalyDomainEmail(user.email, project.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return { ok: true };
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -4,15 +4,14 @@ import { createClient } from 'redis';
|
|||||||
const runtimeConfig = useRuntimeConfig();
|
const runtimeConfig = useRuntimeConfig();
|
||||||
|
|
||||||
export const DATA_EXPIRE_TIME = 30;
|
export const DATA_EXPIRE_TIME = 30;
|
||||||
export const TIMELINE_EXPIRE_TIME = 60 * 5;
|
export const TIMELINE_EXPIRE_TIME = 60;
|
||||||
export const COUNTS_EXPIRE_TIME = 10;
|
export const COUNTS_EXPIRE_TIME = 10;
|
||||||
|
|
||||||
export const COUNTS_OLD_SESSIONS_EXPIRE_TIME = 60 * 5;
|
|
||||||
export const COUNTS_SESSIONS_EXPIRE_TIME = 60 * 3;
|
export const COUNTS_SESSIONS_EXPIRE_TIME = 60 * 3;
|
||||||
|
|
||||||
export const EVENT_NAMES_EXPIRE_TIME = 60;
|
export const EVENT_NAMES_EXPIRE_TIME = 60;
|
||||||
|
|
||||||
export const EVENT_METADATA_FIELDS_EXPIRE_TIME = 120;
|
export const EVENT_METADATA_FIELDS_EXPIRE_TIME = 30;
|
||||||
|
|
||||||
|
|
||||||
export class Redis {
|
export class Redis {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getPlanFromId, getPlanFromTag } from '@data/PREMIUM';
|
import { getPlanFromId, getPlanFromTag, PREMIUM_TAG } from '@data/PREMIUM';
|
||||||
import Stripe from 'stripe';
|
import Stripe from 'stripe';
|
||||||
|
|
||||||
class StripeService {
|
class StripeService {
|
||||||
@@ -133,9 +133,23 @@ class StripeService {
|
|||||||
return deleted;
|
return deleted;
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOneTimeCoupon() {
|
async createStripeCode(plan: PREMIUM_TAG) {
|
||||||
if (this.disabledMode) return;
|
if (this.disabledMode) return;
|
||||||
if (!this.stripe) throw Error('Stripe not initialized');
|
if (!this.stripe) throw Error('Stripe not initialized');
|
||||||
|
|
||||||
|
const INCUBATION_COUPON = 'sDD7Weh3';
|
||||||
|
|
||||||
|
if (plan === 'INCUBATION') {
|
||||||
|
await this.stripe.promotionCodes.create({
|
||||||
|
coupon: INCUBATION_COUPON,
|
||||||
|
active: true,
|
||||||
|
code: 'TESTCACCA1',
|
||||||
|
max_redemptions: 1,
|
||||||
|
})
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOneTimeSubscriptionDummy(customer_id: string, planId: number) {
|
async createOneTimeSubscriptionDummy(customer_id: string, planId: number) {
|
||||||
|
|||||||
@@ -61,4 +61,10 @@ export function fillAndMergeTimelineAggregation(timeline: { _id: string, count:
|
|||||||
const filledDates = DateService.fillDates(timeline.map(e => e._id), slice);
|
const filledDates = DateService.fillDates(timeline.map(e => e._id), slice);
|
||||||
const merged = DateService.mergeFilledDates(filledDates, timeline, '_id', slice, { count: 0 });
|
const merged = DateService.mergeFilledDates(filledDates, timeline, '_id', slice, { count: 0 });
|
||||||
return merged;
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fillAndMergeTimelineAggregationV2(timeline: { _id: string, count: number }[], slice: Slice, from: string, to: string) {
|
||||||
|
const filledDates = DateService.createBetweenDates(from, to, slice);
|
||||||
|
const merged = DateService.mergeFilledDates(filledDates.dates, timeline, '_id', slice, { count: 0 });
|
||||||
|
return merged;
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,7 @@ import { RedisStreamService } from "@services/RedisStreamService";
|
|||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
const allowAnyType = () => true;
|
const allowAnyType = () => true;
|
||||||
const jsonOptions = { limit: '5mb', type: allowAnyType }
|
const jsonOptions = { limit: '25kb', type: allowAnyType }
|
||||||
|
|
||||||
const streamName = requireEnv('STREAM_NAME');
|
const streamName = requireEnv('STREAM_NAME');
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const app = express();
|
|||||||
app.use(cors());
|
app.use(cors());
|
||||||
|
|
||||||
const allowAnyType = () => true;
|
const allowAnyType = () => true;
|
||||||
const jsonOptions = { limit: '5mb', type: allowAnyType }
|
const jsonOptions = { limit: '25kb', type: allowAnyType }
|
||||||
|
|
||||||
const streamName = requireEnv('STREAM_NAME');
|
const streamName = requireEnv('STREAM_NAME');
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ export type PREMIUM_DATA = {
|
|||||||
AI_MESSAGE_LIMIT: number,
|
AI_MESSAGE_LIMIT: number,
|
||||||
PRICE: string,
|
PRICE: string,
|
||||||
PRICE_TEST: string,
|
PRICE_TEST: string,
|
||||||
ID: number
|
ID: number,
|
||||||
|
COST: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PREMIUM_PLAN: Record<PREMIUM_TAG, PREMIUM_DATA> = {
|
export const PREMIUM_PLAN: Record<PREMIUM_TAG, PREMIUM_DATA> = {
|
||||||
@@ -32,98 +33,116 @@ export const PREMIUM_PLAN: Record<PREMIUM_TAG, PREMIUM_DATA> = {
|
|||||||
COUNT_LIMIT: 5_000,
|
COUNT_LIMIT: 5_000,
|
||||||
AI_MESSAGE_LIMIT: 10,
|
AI_MESSAGE_LIMIT: 10,
|
||||||
PRICE: 'price_1POKCMB2lPUiVs9VLe3QjIHl',
|
PRICE: 'price_1POKCMB2lPUiVs9VLe3QjIHl',
|
||||||
PRICE_TEST: 'price_1PNbHYB2lPUiVs9VZP32xglF'
|
PRICE_TEST: 'price_1PNbHYB2lPUiVs9VZP32xglF',
|
||||||
|
COST: 0
|
||||||
},
|
},
|
||||||
PLAN_1: {
|
PLAN_1: {
|
||||||
ID: 1,
|
ID: 1,
|
||||||
COUNT_LIMIT: 150_000,
|
COUNT_LIMIT: 150_000,
|
||||||
AI_MESSAGE_LIMIT: 100,
|
AI_MESSAGE_LIMIT: 100,
|
||||||
PRICE: 'price_1POKCOB2lPUiVs9VC13s2rQw',
|
PRICE: 'price_1POKCOB2lPUiVs9VC13s2rQw',
|
||||||
PRICE_TEST: 'price_1PNZjVB2lPUiVs9VrsTbJL04'
|
PRICE_TEST: 'price_1PNZjVB2lPUiVs9VrsTbJL04',
|
||||||
|
COST: 0
|
||||||
},
|
},
|
||||||
PLAN_2: {
|
PLAN_2: {
|
||||||
ID: 2,
|
ID: 2,
|
||||||
COUNT_LIMIT: 500_000,
|
COUNT_LIMIT: 500_000,
|
||||||
AI_MESSAGE_LIMIT: 5_000,
|
AI_MESSAGE_LIMIT: 5_000,
|
||||||
PRICE: 'price_1POKCKB2lPUiVs9Vol8XOmhW',
|
PRICE: 'price_1POKCKB2lPUiVs9Vol8XOmhW',
|
||||||
PRICE_TEST: 'price_1POK34B2lPUiVs9VIROb0IIV'
|
PRICE_TEST: 'price_1POK34B2lPUiVs9VIROb0IIV',
|
||||||
|
COST: 0
|
||||||
},
|
},
|
||||||
CUSTOM_1: {
|
CUSTOM_1: {
|
||||||
ID: 1001,
|
ID: 1001,
|
||||||
COUNT_LIMIT: 10_000_000,
|
COUNT_LIMIT: 10_000_000,
|
||||||
AI_MESSAGE_LIMIT: 100_000,
|
AI_MESSAGE_LIMIT: 100_000,
|
||||||
PRICE: 'price_1POKZyB2lPUiVs9VMAY6jXTV',
|
PRICE: 'price_1POKZyB2lPUiVs9VMAY6jXTV',
|
||||||
PRICE_TEST: ''
|
PRICE_TEST: '',
|
||||||
|
COST: 0
|
||||||
},
|
},
|
||||||
INCUBATION: {
|
INCUBATION: {
|
||||||
ID: 101,
|
ID: 101,
|
||||||
COUNT_LIMIT: 50_000,
|
COUNT_LIMIT: 50_000,
|
||||||
AI_MESSAGE_LIMIT: 30,
|
AI_MESSAGE_LIMIT: 30,
|
||||||
PRICE: 'price_1PdsyzB2lPUiVs9V4J246Jw0',
|
PRICE: 'price_1PdsyzB2lPUiVs9V4J246Jw0',
|
||||||
PRICE_TEST: ''
|
PRICE_TEST: '',
|
||||||
|
COST: 499
|
||||||
},
|
},
|
||||||
ACCELERATION: {
|
ACCELERATION: {
|
||||||
ID: 102,
|
ID: 102,
|
||||||
COUNT_LIMIT: 150_000,
|
COUNT_LIMIT: 150_000,
|
||||||
AI_MESSAGE_LIMIT: 100,
|
AI_MESSAGE_LIMIT: 100,
|
||||||
PRICE: 'price_1Pdt5bB2lPUiVs9VhkuCouEt',
|
PRICE: 'price_1Pdt5bB2lPUiVs9VhkuCouEt',
|
||||||
PRICE_TEST: ''
|
PRICE_TEST: '',
|
||||||
|
COST: 999
|
||||||
},
|
},
|
||||||
GROWTH: {
|
GROWTH: {
|
||||||
ID: 103,
|
ID: 103,
|
||||||
COUNT_LIMIT: 500_000,
|
COUNT_LIMIT: 500_000,
|
||||||
AI_MESSAGE_LIMIT: 3_000,
|
AI_MESSAGE_LIMIT: 3_000,
|
||||||
PRICE: 'price_1PdszrB2lPUiVs9VIdkT3thv',
|
PRICE: 'price_1PdszrB2lPUiVs9VIdkT3thv',
|
||||||
PRICE_TEST: ''
|
PRICE_TEST: '',
|
||||||
|
COST: 2999
|
||||||
},
|
},
|
||||||
EXPANSION: {
|
EXPANSION: {
|
||||||
ID: 104,
|
ID: 104,
|
||||||
COUNT_LIMIT: 1_000_000,
|
COUNT_LIMIT: 1_000_000,
|
||||||
AI_MESSAGE_LIMIT: 5_000,
|
AI_MESSAGE_LIMIT: 5_000,
|
||||||
PRICE: 'price_1Pdt0xB2lPUiVs9V0Rdt80Fe',
|
PRICE: 'price_1Pdt0xB2lPUiVs9V0Rdt80Fe',
|
||||||
PRICE_TEST: ''
|
PRICE_TEST: '',
|
||||||
|
COST: 5999
|
||||||
},
|
},
|
||||||
SCALING: {
|
SCALING: {
|
||||||
ID: 105,
|
ID: 105,
|
||||||
COUNT_LIMIT: 2_500_000,
|
COUNT_LIMIT: 2_500_000,
|
||||||
AI_MESSAGE_LIMIT: 10_000,
|
AI_MESSAGE_LIMIT: 10_000,
|
||||||
PRICE: 'price_1Pdt1UB2lPUiVs9VUmxntSwZ',
|
PRICE: 'price_1Pdt1UB2lPUiVs9VUmxntSwZ',
|
||||||
PRICE_TEST: ''
|
PRICE_TEST: '',
|
||||||
|
COST: 9999
|
||||||
},
|
},
|
||||||
UNICORN: {
|
UNICORN: {
|
||||||
ID: 106,
|
ID: 106,
|
||||||
COUNT_LIMIT: 5_000_000,
|
COUNT_LIMIT: 5_000_000,
|
||||||
AI_MESSAGE_LIMIT: 20_000,
|
AI_MESSAGE_LIMIT: 20_000,
|
||||||
PRICE: 'price_1Pdt2LB2lPUiVs9VGBFAIG9G',
|
PRICE: 'price_1Pdt2LB2lPUiVs9VGBFAIG9G',
|
||||||
PRICE_TEST: ''
|
PRICE_TEST: '',
|
||||||
|
COST: 14999
|
||||||
},
|
},
|
||||||
LIFETIME_GROWTH_ONETIME: {
|
LIFETIME_GROWTH_ONETIME: {
|
||||||
ID: 2001,
|
ID: 2001,
|
||||||
COUNT_LIMIT: 500_000,
|
COUNT_LIMIT: 500_000,
|
||||||
AI_MESSAGE_LIMIT: 3_000,
|
AI_MESSAGE_LIMIT: 3_000,
|
||||||
PRICE: 'price_1PvewGB2lPUiVs9VLheJC8s1',
|
PRICE: 'price_1PvewGB2lPUiVs9VLheJC8s1',
|
||||||
PRICE_TEST: 'price_1Pvf7LB2lPUiVs9VMFNyzpim'
|
PRICE_TEST: 'price_1Pvf7LB2lPUiVs9VMFNyzpim',
|
||||||
|
COST: 239900
|
||||||
},
|
},
|
||||||
GROWTH_DUMMY: {
|
GROWTH_DUMMY: {
|
||||||
ID: 5001,
|
ID: 5001,
|
||||||
COUNT_LIMIT: 500_000,
|
COUNT_LIMIT: 500_000,
|
||||||
AI_MESSAGE_LIMIT: 3_000,
|
AI_MESSAGE_LIMIT: 3_000,
|
||||||
PRICE: 'price_1PvgoRB2lPUiVs9VC51YBT7J',
|
PRICE: 'price_1PvgoRB2lPUiVs9VC51YBT7J',
|
||||||
PRICE_TEST: 'price_1PvgRTB2lPUiVs9V3kFSNC3G'
|
PRICE_TEST: 'price_1PvgRTB2lPUiVs9V3kFSNC3G',
|
||||||
|
COST: 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomPremiumPriceModel.find({}).then(custom_prices => {
|
try {
|
||||||
for (const custom_price of custom_prices) {
|
|
||||||
PREMIUM_PLAN[custom_price.tag] = {
|
CustomPremiumPriceModel.find({}).then(custom_prices => {
|
||||||
ID: custom_price.price_id,
|
for (const custom_price of custom_prices) {
|
||||||
COUNT_LIMIT: custom_price.count_limit,
|
PREMIUM_PLAN[custom_price.tag] = {
|
||||||
AI_MESSAGE_LIMIT: custom_price.ai_message_limit,
|
ID: custom_price.price_id,
|
||||||
PRICE: custom_price.price,
|
COUNT_LIMIT: custom_price.count_limit,
|
||||||
PRICE_TEST: custom_price.price_test || ''
|
AI_MESSAGE_LIMIT: custom_price.ai_message_limit,
|
||||||
|
PRICE: custom_price.price,
|
||||||
|
PRICE_TEST: custom_price.price_test || ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
|
} catch (ex) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export function getPlanFromTag(tag: PREMIUM_TAG) {
|
export function getPlanFromTag(tag: PREMIUM_TAG) {
|
||||||
|
|||||||
16
shared/schema/anomalies/AnomalyDomainSchema.ts
Normal file
16
shared/schema/anomalies/AnomalyDomainSchema.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { model, Schema, Types } from 'mongoose';
|
||||||
|
|
||||||
|
|
||||||
|
export type TAnomalyDomain = {
|
||||||
|
project_id: Schema.Types.ObjectId
|
||||||
|
domain: string,
|
||||||
|
created_at: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnomalyDomainSchema = new Schema<TAnomalyDomain>({
|
||||||
|
project_id: { type: Types.ObjectId, required: true },
|
||||||
|
domain: { type: String, required: true },
|
||||||
|
created_at: { type: Date, required: true },
|
||||||
|
})
|
||||||
|
|
||||||
|
export const AnomalyDomainModel = model<TAnomalyDomain>('anomaly_domains', AnomalyDomainSchema);
|
||||||
16
shared/schema/anomalies/AnomalyEventsSchema.ts
Normal file
16
shared/schema/anomalies/AnomalyEventsSchema.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { model, Schema, Types } from 'mongoose';
|
||||||
|
|
||||||
|
|
||||||
|
export type TAnomalyEvents = {
|
||||||
|
project_id: Schema.Types.ObjectId
|
||||||
|
eventDate: Date,
|
||||||
|
created_at: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnomalyEventsSchema = new Schema<TAnomalyEvents>({
|
||||||
|
project_id: { type: Types.ObjectId, required: true },
|
||||||
|
eventDate: { type: Date, required: true },
|
||||||
|
created_at: { type: Date, required: true },
|
||||||
|
})
|
||||||
|
|
||||||
|
export const AnomalyEventsModel = model<TAnomalyEvents>('anomaly_events', AnomalyEventsSchema);
|
||||||
16
shared/schema/anomalies/AnomalyVisitSchema.ts
Normal file
16
shared/schema/anomalies/AnomalyVisitSchema.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { model, Schema, Types } from 'mongoose';
|
||||||
|
|
||||||
|
|
||||||
|
export type TAnomalyVisit = {
|
||||||
|
project_id: Schema.Types.ObjectId
|
||||||
|
visitDate: Date,
|
||||||
|
created_at: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnomalyVisitSchema = new Schema<TAnomalyVisit>({
|
||||||
|
project_id: { type: Types.ObjectId, required: true },
|
||||||
|
visitDate: { type: Date, required: true },
|
||||||
|
created_at: { type: Date, required: true },
|
||||||
|
})
|
||||||
|
|
||||||
|
export const AnomalyVisitModel = model<TAnomalyVisit>('anomaly_visits', AnomalyVisitSchema);
|
||||||
@@ -102,6 +102,18 @@ class DateService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createBetweenDates(from: string, to: string, slice: Slice) {
|
||||||
|
let start = dayjs(from);
|
||||||
|
const end = dayjs(to);
|
||||||
|
const filledDates: dayjs.Dayjs[] = [];
|
||||||
|
while (start.isBefore(end) || start.isSame(end)) {
|
||||||
|
filledDates.push(start);
|
||||||
|
start = start.add(1, slice);
|
||||||
|
}
|
||||||
|
return { dates: filledDates, from, to };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fillDates(dates: string[], slice: Slice) {
|
fillDates(dates: string[], slice: Slice) {
|
||||||
const allDates: dayjs.Dayjs[] = [];
|
const allDates: dayjs.Dayjs[] = [];
|
||||||
const firstDate = dayjs(dates.at(0));
|
const firstDate = dayjs(dates.at(0));
|
||||||
@@ -109,7 +121,7 @@ class DateService {
|
|||||||
let currentDate = firstDate.clone();
|
let currentDate = firstDate.clone();
|
||||||
|
|
||||||
allDates.push(currentDate);
|
allDates.push(currentDate);
|
||||||
|
|
||||||
while (currentDate.isBefore(lastDate, slice)) {
|
while (currentDate.isBefore(lastDate, slice)) {
|
||||||
currentDate = currentDate.add(1, slice);
|
currentDate = currentDate.add(1, slice);
|
||||||
allDates.push(currentDate);
|
allDates.push(currentDate);
|
||||||
@@ -121,7 +133,7 @@ class DateService {
|
|||||||
mergeFilledDates<T extends Record<string, any>, K extends keyof T>(dates: dayjs.Dayjs[], items: T[], dateField: K, slice: Slice, fillData: Omit<T, K>) {
|
mergeFilledDates<T extends Record<string, any>, K extends keyof T>(dates: dayjs.Dayjs[], items: T[], dateField: K, slice: Slice, fillData: Omit<T, K>) {
|
||||||
const result = new Array<T>();
|
const result = new Array<T>();
|
||||||
for (const date of dates) {
|
for (const date of dates) {
|
||||||
const item = items.find(e => dayjs(e[dateField]).isSame(date), slice);
|
const item = items.find(e => dayjs(e[dateField]).isSame(date, slice));
|
||||||
result.push(item ?? { ...fillData, [dateField]: date.format() } as T);
|
result.push(item ?? { ...fillData, [dateField]: date.format() } as T);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import { WELCOME_EMAIL } from './email_templates/WelcomeEmail';
|
|||||||
import { LIMIT_50_EMAIL } from './email_templates/Limit50Email';
|
import { LIMIT_50_EMAIL } from './email_templates/Limit50Email';
|
||||||
import { LIMIT_90_EMAIL } from './email_templates/Limit90Email';
|
import { LIMIT_90_EMAIL } from './email_templates/Limit90Email';
|
||||||
import { LIMIT_MAX_EMAIL } from './email_templates/LimitMaxEmail';
|
import { LIMIT_MAX_EMAIL } from './email_templates/LimitMaxEmail';
|
||||||
|
import { PURCHASE_EMAIL } from './email_templates/PurchaseEmail';
|
||||||
|
import { ANOMALY_VISITS_EVENTS_EMAIL } from './email_templates/AnomalyUsageEmail';
|
||||||
|
import { ANOMALY_DOMAIN_EMAIL } from './email_templates/AnomalyDomainEmail';
|
||||||
|
|
||||||
|
|
||||||
class EmailService {
|
class EmailService {
|
||||||
@@ -17,7 +20,7 @@ class EmailService {
|
|||||||
try {
|
try {
|
||||||
const sendSmtpEmail = new SendSmtpEmail();
|
const sendSmtpEmail = new SendSmtpEmail();
|
||||||
sendSmtpEmail.subject = "You've reached 50% limit on Litlyx";
|
sendSmtpEmail.subject = "You've reached 50% limit on Litlyx";
|
||||||
sendSmtpEmail.sender = { "name": "Litlyx", "email": "no-reply@litlyx.com" };
|
sendSmtpEmail.sender = { "name": "Litlyx", "email": "help@litlyx.com" };
|
||||||
sendSmtpEmail.to = [{ "email": target }];
|
sendSmtpEmail.to = [{ "email": target }];
|
||||||
|
|
||||||
sendSmtpEmail.htmlContent = LIMIT_50_EMAIL
|
sendSmtpEmail.htmlContent = LIMIT_50_EMAIL
|
||||||
@@ -36,7 +39,7 @@ class EmailService {
|
|||||||
try {
|
try {
|
||||||
const sendSmtpEmail = new SendSmtpEmail();
|
const sendSmtpEmail = new SendSmtpEmail();
|
||||||
sendSmtpEmail.subject = "You've reached 90% limit on Litlyx";
|
sendSmtpEmail.subject = "You've reached 90% limit on Litlyx";
|
||||||
sendSmtpEmail.sender = { "name": "Litlyx", "email": "no-reply@litlyx.com" };
|
sendSmtpEmail.sender = { "name": "Litlyx", "email": "help@litlyx.com" };
|
||||||
sendSmtpEmail.to = [{ "email": target }];
|
sendSmtpEmail.to = [{ "email": target }];
|
||||||
sendSmtpEmail.htmlContent = LIMIT_90_EMAIL
|
sendSmtpEmail.htmlContent = LIMIT_90_EMAIL
|
||||||
.replace(/\[Project Name\]/, projectName)
|
.replace(/\[Project Name\]/, projectName)
|
||||||
@@ -53,7 +56,7 @@ class EmailService {
|
|||||||
try {
|
try {
|
||||||
const sendSmtpEmail = new SendSmtpEmail();
|
const sendSmtpEmail = new SendSmtpEmail();
|
||||||
sendSmtpEmail.subject = "You've reached your limit on Litlyx!";
|
sendSmtpEmail.subject = "You've reached your limit on Litlyx!";
|
||||||
sendSmtpEmail.sender = { "name": "Litlyx", "email": "no-reply@litlyx.com" };
|
sendSmtpEmail.sender = { "name": "Litlyx", "email": "help@litlyx.com" };
|
||||||
sendSmtpEmail.to = [{ "email": target }];
|
sendSmtpEmail.to = [{ "email": target }];
|
||||||
sendSmtpEmail.htmlContent = LIMIT_MAX_EMAIL
|
sendSmtpEmail.htmlContent = LIMIT_MAX_EMAIL
|
||||||
.replace(/\[Project Name\]/, projectName)
|
.replace(/\[Project Name\]/, projectName)
|
||||||
@@ -70,7 +73,7 @@ class EmailService {
|
|||||||
try {
|
try {
|
||||||
const sendSmtpEmail = new SendSmtpEmail();
|
const sendSmtpEmail = new SendSmtpEmail();
|
||||||
sendSmtpEmail.subject = "Welcome to Litlyx!";
|
sendSmtpEmail.subject = "Welcome to Litlyx!";
|
||||||
sendSmtpEmail.sender = { "name": "Litlyx", "email": "no-reply@litlyx.com" };
|
sendSmtpEmail.sender = { "name": "Litlyx", "email": "help@litlyx.com" };
|
||||||
sendSmtpEmail.to = [{ "email": target }];
|
sendSmtpEmail.to = [{ "email": target }];
|
||||||
sendSmtpEmail.htmlContent = WELCOME_EMAIL;
|
sendSmtpEmail.htmlContent = WELCOME_EMAIL;
|
||||||
await this.apiInstance.sendTransacEmail(sendSmtpEmail);
|
await this.apiInstance.sendTransacEmail(sendSmtpEmail);
|
||||||
@@ -81,6 +84,57 @@ class EmailService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async sendPurchaseEmail(target: string, projectName: string) {
|
||||||
|
try {
|
||||||
|
const sendSmtpEmail = new SendSmtpEmail();
|
||||||
|
sendSmtpEmail.subject = "Thank You for Upgrading Your Litlyx Plan!";
|
||||||
|
sendSmtpEmail.sender = { "name": "Litlyx", "email": "help@litlyx.com" };
|
||||||
|
sendSmtpEmail.to = [{ "email": target }];
|
||||||
|
sendSmtpEmail.htmlContent = PURCHASE_EMAIL
|
||||||
|
.replace(/\[Project Name\]/, projectName)
|
||||||
|
.toString();;
|
||||||
|
await this.apiInstance.sendTransacEmail(sendSmtpEmail);
|
||||||
|
return true;
|
||||||
|
} catch (ex) {
|
||||||
|
console.error('ERROR SENDING EMAIL', ex);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendAnomalyVisitsEventsEmail(target: string, projectName: string) {
|
||||||
|
try {
|
||||||
|
const sendSmtpEmail = new SendSmtpEmail();
|
||||||
|
sendSmtpEmail.subject = "🚨 Unexpected Activity Detected by our AI";
|
||||||
|
sendSmtpEmail.sender = { "name": "Litlyx", "email": "help@litlyx.com" };
|
||||||
|
sendSmtpEmail.to = [{ "email": target }];
|
||||||
|
sendSmtpEmail.htmlContent = ANOMALY_VISITS_EVENTS_EMAIL
|
||||||
|
.replace(/\[Project Name\]/, projectName)
|
||||||
|
.toString();;
|
||||||
|
await this.apiInstance.sendTransacEmail(sendSmtpEmail);
|
||||||
|
return true;
|
||||||
|
} catch (ex) {
|
||||||
|
console.error('ERROR SENDING EMAIL', ex);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendAnomalyDomainEmail(target: string, projectName: string) {
|
||||||
|
try {
|
||||||
|
const sendSmtpEmail = new SendSmtpEmail();
|
||||||
|
sendSmtpEmail.subject = "🚨 Anomaly detected by our AI";
|
||||||
|
sendSmtpEmail.sender = { "name": "Litlyx", "email": "help@litlyx.com" };
|
||||||
|
sendSmtpEmail.to = [{ "email": target }];
|
||||||
|
sendSmtpEmail.htmlContent = ANOMALY_DOMAIN_EMAIL
|
||||||
|
.replace(/\[Project Name\]/, projectName)
|
||||||
|
.toString();;
|
||||||
|
await this.apiInstance.sendTransacEmail(sendSmtpEmail);
|
||||||
|
return true;
|
||||||
|
} catch (ex) {
|
||||||
|
console.error('ERROR SENDING EMAIL', ex);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const instance = new EmailService();
|
const instance = new EmailService();
|
||||||
|
|||||||
@@ -36,7 +36,11 @@ export class RedisStreamService {
|
|||||||
setTimeout(() => this.readingLoop(options, processFunction), 1);
|
setTimeout(() => this.readingLoop(options, processFunction), 1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await processFunction(result);
|
try {
|
||||||
|
await processFunction(result);
|
||||||
|
} catch (ex) {
|
||||||
|
console.error('Error on processing function');
|
||||||
|
}
|
||||||
RedisStreamService.processed++;
|
RedisStreamService.processed++;
|
||||||
await new Promise(r => setTimeout(r, options.delay?.base || 100));
|
await new Promise(r => setTimeout(r, options.delay?.base || 100));
|
||||||
setTimeout(() => this.readingLoop(options, processFunction), 1);
|
setTimeout(() => this.readingLoop(options, processFunction), 1);
|
||||||
@@ -46,10 +50,12 @@ export class RedisStreamService {
|
|||||||
static async startReadingLoop(options: ReadingLoopOptions, processFunction: (content: Record<string, string>) => Promise<any>) {
|
static async startReadingLoop(options: ReadingLoopOptions, processFunction: (content: Record<string, string>) => Promise<any>) {
|
||||||
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
console.log('Processed:', (RedisStreamService.processed / 30).toFixed(), '/s');
|
if (RedisStreamService.processed > 0) {
|
||||||
RedisStreamService.processed = 0;
|
console.log('Processed:', (RedisStreamService.processed / 30).toFixed(2), '/s');
|
||||||
|
RedisStreamService.processed = 0;
|
||||||
|
}
|
||||||
}, 30_000)
|
}, 30_000)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('Start reading loop');
|
console.log('Start reading loop');
|
||||||
await this.client.xGroupCreate(options.streamName, 'broker', '0', { MKSTREAM: true, });
|
await this.client.xGroupCreate(options.streamName, 'broker', '0', { MKSTREAM: true, });
|
||||||
|
|||||||
43
shared/services/email_templates/AnomalyDomainEmail.ts
Normal file
43
shared/services/email_templates/AnomalyDomainEmail.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
export const ANOMALY_DOMAIN_EMAIL = `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>❗️ Anomaly detected by our AI</title>
|
||||||
|
</head>
|
||||||
|
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||||||
|
|
||||||
|
<!-- Email Content -->
|
||||||
|
|
||||||
|
<p>Dear User,</p>
|
||||||
|
|
||||||
|
<p>We wanted to let you know that <strong>[Project Name]</strong> on <strong>Litlyx</strong> has an anomaly that our AI agent detected.</p>
|
||||||
|
|
||||||
|
<p>You can analyze a suspicious DNS on your Litlyx dashboard. We put a symbol next to each suspicious DNS to let users know something might be wrong!</p>
|
||||||
|
|
||||||
|
<h3>What can I do?</h3>
|
||||||
|
|
||||||
|
<p>To resolve this issue, you should reach out to the webmasters of the websites that have duplicated your content and request them to remove it or give you proper attribution (if available).</p>
|
||||||
|
|
||||||
|
<p>You can also use <a href="https://www.whois.com/whois/" style="color: #D32F2F; text-decoration: none;">https://www.whois.com/whois/</a> to get the contact details of the webmaster or domain owner.</p>
|
||||||
|
|
||||||
|
<p>If webmasters don't respond or cooperate, <strong>you can file a DMCA complaint here:</strong> <a href="https://support.google.com/legal/answer/3110420?hl=en" style="color: #D32F2F; text-decoration: none;">https://support.google.com/legal/answer/3110420?hl=en</a> <strong>with Google to request the removal of the duplicate content from their search results.</strong></p>
|
||||||
|
|
||||||
|
<h3>Please refer to this for more information:</h3>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li><a href="https://support.google.com/legal/answer/3110420?hl=en&sjid=14235884554806745995-AP&authuser=2" style="color: #D32F2F; text-decoration: none;">Report Content for Legal Reasons</a></li>
|
||||||
|
<li><a href="https://www.dmca.com/FAQ/How-can-I-get-a-webpage-removed-from-Google-search-results" style="color: #D32F2F; text-decoration: none;">How can I get a webpage removed from Google search results?</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>Your safety is our main priority.</p>
|
||||||
|
|
||||||
|
<p>Thank you for choosing Litlyx every day as your analytics tool!</p>
|
||||||
|
|
||||||
|
<p>Antonio,</p>
|
||||||
|
<p>CEO | Litlyx</p>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`
|
||||||
42
shared/services/email_templates/AnomalyUsageEmail.ts
Normal file
42
shared/services/email_templates/AnomalyUsageEmail.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
export const ANOMALY_VISITS_EVENTS_EMAIL = `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>🚨 Unexpected Activity Detected by our AI</title>
|
||||||
|
</head>
|
||||||
|
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||||||
|
|
||||||
|
<!-- Email Content -->
|
||||||
|
|
||||||
|
<p>Dear User,</p>
|
||||||
|
|
||||||
|
<p>We wanted to let you know that <strong>[Project Name]</strong> on <strong>Litlyx</strong> is receiving an unexpected amount of visits or events. This could indicate unusual activity that might require your attention.</p>
|
||||||
|
|
||||||
|
<p>If this spike in activity is expected, there’s no need to worry. However, if you believe this could be unexpected or suspicious, we recommend taking a closer look at your data on the <strong>Litlyx Dashboard</strong>. You can analyze your recent traffic and event logs to identify any irregularities or potential issues.</p>
|
||||||
|
|
||||||
|
<h3>What can I do?</h3>
|
||||||
|
|
||||||
|
<p>To better understand the situation, you can:</p>
|
||||||
|
|
||||||
|
<ol>
|
||||||
|
<li>Review your traffic sources to see where the visits or events are coming from.</li>
|
||||||
|
<li>Check for any unexpected patterns, such as a high number of visits from unknown sources or abnormal event triggers.</li>
|
||||||
|
<li>Check your code to find bugs on a specific action that is triggered in loops.</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<p>If you need help understanding this activity or have any concerns, feel free to reach out to our support team at <a href="mailto:help@litlyx.com" style="color: #D32F2F; text-decoration: none;"><strong>help@litlyx.com</strong></a>. We are here to assist you!</p>
|
||||||
|
|
||||||
|
<p><strong>Your safety and data integrity are our top priorities.</strong></p>
|
||||||
|
|
||||||
|
<p>Thank you for trusting Litlyx as your analytics tool!</p>
|
||||||
|
|
||||||
|
<p>Best regards,</p>
|
||||||
|
|
||||||
|
<p>Antonio,</p>
|
||||||
|
<p>CEO | Litlyx</p>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`
|
||||||
45
shared/services/email_templates/PurchaseEmail.ts
Normal file
45
shared/services/email_templates/PurchaseEmail.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
|
||||||
|
export const PURCHASE_EMAIL = `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Thank You for Upgrading Your Litlyx Plan!</title>
|
||||||
|
</head>
|
||||||
|
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||||||
|
|
||||||
|
<!-- Email Content -->
|
||||||
|
|
||||||
|
<p>Dear User,</p>
|
||||||
|
|
||||||
|
<p>We are thrilled to inform you that <strong>[Project Name]</strong> on <strong>Litlyx</strong> has successfully been upgraded to a higher plan! Thank you for choosing to elevate your experience with us and for believing in our project.</p>
|
||||||
|
|
||||||
|
<p>We appreciate your trust in Litlyx and are committed to providing you with the best analytics experience. Your support helps us to continually improve our platform and bring new features to make your analytics journey even better.</p>
|
||||||
|
|
||||||
|
<p>You can find your current plan details and download your invoices under <strong>Settings > Billing Tab</strong>.</p>
|
||||||
|
|
||||||
|
<h3>What does this mean for you?</h3>
|
||||||
|
|
||||||
|
<p>With your upgraded plan, you can now enjoy more data collection, advanced features, and additional benefits to enhance your data analysis capabilities:</p>
|
||||||
|
|
||||||
|
<ol>
|
||||||
|
<li>Access to more storage and increased data limits.</li>
|
||||||
|
<li>Advanced analytics tools like IP-Company Matching, download CSV for your raw data, AI insights, and more.</li>
|
||||||
|
<li>Priority support to help you make the most of your Litlyx experience on Slack or Discord!</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<p>If you have any questions about your new plan or need assistance, feel free to reach out to our support team at <a href="mailto:help@litlyx.com" style="color: #28a745; text-decoration: none;"><strong>help@litlyx.com</strong></a>. We’re here to help you make the most out of your upgraded plan!</p>
|
||||||
|
|
||||||
|
<p><strong>Thank you for using Litlyx every day as your analytics tool and for being a part of our journey.</strong></p>
|
||||||
|
|
||||||
|
<p>We look forward to continuing to support your growth and success!</p>
|
||||||
|
|
||||||
|
<p>Best regards,</p>
|
||||||
|
|
||||||
|
<p>Antonio,</p>
|
||||||
|
<p>CEO | Litlyx</p>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>You’ve reached 50% limit on Litlyx</title>
|
|
||||||
</head>
|
|
||||||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
|
||||||
|
|
||||||
<!-- Email Content -->
|
|
||||||
<h2 style="color: #FF5733;">You’ve reached 50% limit on Litlyx</h2>
|
|
||||||
|
|
||||||
<p>Dear User,</p>
|
|
||||||
|
|
||||||
<p>We wanted to let you know that <strong>[Project Name]</strong> on <strong>Litlyx</strong> has reached 50% of its data collection limit for this month.</p>
|
|
||||||
|
|
||||||
<p>To avoid losing precious data, please remember to monitor your usage on the <strong>Litlyx Dashboard</strong>. You can find your current usage details under <strong>Settings > Billing Tab</strong>.</p>
|
|
||||||
|
|
||||||
<p>If you need more data collection storage, you may consider upgrading your plan to get additional benefits and ensure uninterrupted data collection.</p>
|
|
||||||
|
|
||||||
<p>Feel free to reply to this email or contact us at <a href="mailto:help@litlyx.com" style="color: #FF5733; text-decoration: none;">help@litlyx.com</a> if you have any questions or need assistance.</p>
|
|
||||||
|
|
||||||
<p>Thank you for choosing Litlyx every day as your analytics tool.</p>
|
|
||||||
|
|
||||||
<p>Have a nice day!</p>
|
|
||||||
|
|
||||||
<p>Antonio,</p>
|
|
||||||
<p>CEO | Litlyx</p>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>You’ve reached 90% limit on Litlyx</title>
|
|
||||||
</head>
|
|
||||||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
|
||||||
|
|
||||||
<!-- Email Content -->
|
|
||||||
<h2 style="color: #FF0000;">You’ve reached 90% limit on Litlyx</h2>
|
|
||||||
|
|
||||||
<p>Dear User,</p>
|
|
||||||
|
|
||||||
<p>We wanted to let you know that <strong>[Project Name]</strong> on <strong>Litlyx</strong> has reached 90% of its data collection limit for this month.</p>
|
|
||||||
|
|
||||||
<p>To avoid losing precious data, please remember to monitor your usage on the <strong>Litlyx Dashboard</strong>. You can find your current usage details under <strong>Settings > Billing Tab</strong>.</p>
|
|
||||||
|
|
||||||
<p>If you need more data collection storage, you may consider upgrading your plan to get additional benefits and ensure uninterrupted data collection.</p>
|
|
||||||
|
|
||||||
<p>Feel free to reply to this email or contact us at <a href="mailto:help@litlyx.com" style="color: #FF0000; text-decoration: none;">help@litlyx.com</a> if you have any questions or need assistance.</p>
|
|
||||||
|
|
||||||
<p>Thank you for choosing Litlyx every day as your analytics tool.</p>
|
|
||||||
|
|
||||||
<p>Have a nice day!</p>
|
|
||||||
|
|
||||||
<p>Antonio,</p>
|
|
||||||
<p>CEO | Litlyx</p>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>❗️ You’ve reached your limit on Litlyx!</title>
|
|
||||||
</head>
|
|
||||||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
|
||||||
|
|
||||||
<!-- Email Content -->
|
|
||||||
<h2 style="color: #D32F2F;">❗️ You’ve reached your limit on Litlyx!</h2>
|
|
||||||
|
|
||||||
<p>Dear User,</p>
|
|
||||||
|
|
||||||
<p>We wanted to let you know that <strong>[Project Name]</strong> on <strong>Litlyx</strong> has reached the limit of your current plan.</p>
|
|
||||||
|
|
||||||
<p>To avoid losing precious data, please remember to monitor your usage on the <strong>Litlyx Dashboard</strong>. You can find your current usage details under <strong>Settings > Billing Tab</strong>.</p>
|
|
||||||
|
|
||||||
<p>If you need more data collection storage, you may consider upgrading your plan to get additional benefits and ensure uninterrupted data collection.</p>
|
|
||||||
|
|
||||||
<p>Feel free to reply to this email or contact us at <a href="mailto:help@litlyx.com" style="color: #D32F2F; text-decoration: none;">help@litlyx.com</a> if you have any questions or need assistance.</p>
|
|
||||||
|
|
||||||
<p>Thank you for choosing Litlyx every day as your analytics tool.</p>
|
|
||||||
|
|
||||||
<p>Have a nice day!</p>
|
|
||||||
|
|
||||||
<p>Antonio,</p>
|
|
||||||
<p>CEO | Litlyx</p>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Welcome to Litlyx!</title>
|
|
||||||
</head>
|
|
||||||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
|
||||||
|
|
||||||
<!-- Subject -->
|
|
||||||
<h2 style="color: #007BFF;">Welcome to Litlyx!</h2>
|
|
||||||
|
|
||||||
<p>We’re happy to have you onboard,</p>
|
|
||||||
|
|
||||||
<p>At Litlyx, we’re committed to creating the best analytics collection experience for everybody, starting from developers.</p>
|
|
||||||
|
|
||||||
<p>Here are a few things you can do to get started tracking analytics today:</p>
|
|
||||||
|
|
||||||
<ol>
|
|
||||||
<li><strong><a href="https://dashboard.litlyx.com" style="color: #007BFF; text-decoration: none;">Create a new project</a></strong> – by just naming it</li>
|
|
||||||
<li><strong><a style="color: #0a0a0a; text-decoration: none;">Copy the universal Script</a></strong> – we provide you the snippets to copy in your index.html file and start instantly to track metrics on your website or web app.</li>
|
|
||||||
<li><strong><a style="color: #0a0a0a; text-decoration: none;">Third Step</a></strong> – Encourage engagement or interaction.</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<p>If you have any questions or need support, visit <a href="http://docs.litlyx.com" style="color: #007BFF;">docs.litlyx.com</a>.</p>
|
|
||||||
|
|
||||||
<p>Feel free to reply to this email or reach out to our team at <a href="mailto:help@litlyx.com" style="color: #007BFF;">help@litlyx.com</a>. We’re here to help!</p>
|
|
||||||
|
|
||||||
<p>Link to Discord for developer support: <a href="https://discord.com/invite/9cQykjsmWX" style="color: #007BFF;">https://discord.com/invite/9cQykjsmWX</a></p>
|
|
||||||
|
|
||||||
<p>Thank you for joining us, and we look forward to seeing you around.</p>
|
|
||||||
|
|
||||||
<p>We want to make analytics the freshest thing on the web.</p>
|
|
||||||
|
|
||||||
<p>Antonio,</p>
|
|
||||||
<p>CEO | Litlyx</p>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
Reference in New Issue
Block a user