mirror of
https://github.com/Litlyx/litlyx
synced 2025-12-10 15:58:38 +01:00
new selfhosted version
This commit is contained in:
88
dashboard/components/complex/ai/AiChat.vue
Normal file
88
dashboard/components/complex/ai/AiChat.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
import type { ReadableChatMessage } from '~/pages/ai.vue';
|
||||
import AssistantMessage from './AssistantMessage.vue';
|
||||
import { CircleAlert } from 'lucide-vue-next';
|
||||
|
||||
const ai_chats_component = useTemplateRef<HTMLDivElement>('ai_chats');
|
||||
|
||||
const props = defineProps<{
|
||||
messages?: ReadableChatMessage[],
|
||||
status?: string,
|
||||
}>();
|
||||
|
||||
const emits = defineEmits<{
|
||||
(event: 'downvoted', message_index: number): void;
|
||||
(event: 'chatdeleted'): void;
|
||||
}>();
|
||||
|
||||
function scrollToBottom() {
|
||||
setTimeout(() => {
|
||||
ai_chats_component.value?.scrollTo({ top: 999999, behavior: 'smooth' });
|
||||
}, 150);
|
||||
}
|
||||
watch(props, async () => {
|
||||
scrollToBottom();
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-2 overflow-y-auto overflow-x-hidden" ref="ai_chats">
|
||||
|
||||
|
||||
|
||||
<div v-for="(message, index) of messages" class="flex flex-col relative">
|
||||
|
||||
<div class="w-full flex justify-end" v-if="message.role === 'user'">
|
||||
<div class="border rounded-md p-2 flex flex-col gap-2 flex-end w-[70%] bg-white dark:bg-black">
|
||||
<div class="flex gap-2 items-center">
|
||||
<Label> {{ message.name ?? 'User' }} </Label>
|
||||
<Label class="text-sm text-muted-foreground" v-if="message.created_at">{{ new
|
||||
Date(message.created_at).toLocaleString() }}</Label>
|
||||
</div>
|
||||
<div>
|
||||
{{ message.content }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AssistantMessage v-if="message.role === 'assistant'" @messageRendered="scrollToBottom()"
|
||||
@downvoted="emits('downvoted', $event)" :message="message" :message_index="index">
|
||||
</AssistantMessage>
|
||||
|
||||
</div>
|
||||
|
||||
<div v-if="status?.startsWith('THINKING')" class="text-sm flex items-center gap-2">
|
||||
<Loader class="!size-3"></Loader>
|
||||
{{ status.split(':')[1] }} is thinking...
|
||||
</div>
|
||||
|
||||
<div v-if="status?.startsWith('FUNCTION')" class="text-sm flex items-center gap-2">
|
||||
<Loader class="!size-3"></Loader>
|
||||
{{ status.split(':')[1] }} is calling a function...
|
||||
</div>
|
||||
|
||||
<div v-if="status?.startsWith('FINDING_AGENT')" class="text-sm flex items-center gap-2">
|
||||
<Loader class="!size-3"></Loader>
|
||||
Finding best agents...
|
||||
</div>
|
||||
|
||||
<div v-if="status?.startsWith('ERRORED')" class="flex items-center gap-2">
|
||||
<CircleAlert class="text-orange-300 size-4"></CircleAlert>
|
||||
<div v-if="messages && messages.length < 100"> An error occurred. Please use another chat. </div>
|
||||
<div v-else> Context limit reached </div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<DevOnly>
|
||||
<div class="flex items-center gap-1 text-muted-foreground overflow-hidden">
|
||||
<Icon name="gg:debug" size="20"></Icon>
|
||||
<div v-if="status"> {{ status }} </div>
|
||||
<div v-else> No Status </div>
|
||||
</div>
|
||||
</DevOnly>
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
||||
292
dashboard/components/complex/ai/AssistantMessage.vue
Normal file
292
dashboard/components/complex/ai/AssistantMessage.vue
Normal file
@@ -0,0 +1,292 @@
|
||||
<script lang="ts" setup>
|
||||
import type { MDCNode, MDCParserResult, MDCRoot } from '@nuxtjs/mdc';
|
||||
import { InfoIcon, ThumbsDown, ThumbsUp } from 'lucide-vue-next';
|
||||
import type { ReadableChatMessage } from '~/pages/ai.vue';
|
||||
import AiChart from '~/components/complex/ai/Chart.vue'
|
||||
|
||||
const props = defineProps<{ message: ReadableChatMessage, message_index: number }>();
|
||||
|
||||
const parsedMessage = ref<MDCParserResult>();
|
||||
|
||||
const hidden = ref<boolean>(props.message.downvoted ?? false);
|
||||
|
||||
const emits = defineEmits<{
|
||||
(event: 'messageRendered'): void;
|
||||
(event: 'downvoted', index: number): void;
|
||||
}>();
|
||||
|
||||
|
||||
function removeEmbedImages(data: MDCRoot | MDCNode) {
|
||||
if (data.type !== 'root' && data.type !== 'element') return;
|
||||
if (!data.children) return;
|
||||
const imgChilds = data.children.filter(e => e.type === 'element' && e.tag === 'img');
|
||||
if (imgChilds.length == 0) return data.children.forEach(e => removeEmbedImages(e));
|
||||
for (let i = 0; i < imgChilds.length; i++) {
|
||||
const index = data.children.indexOf(imgChilds[i]);
|
||||
console.log('Index', index)
|
||||
if (index == -1) continue;
|
||||
data.children.splice(index, 1);
|
||||
}
|
||||
return data.children.forEach(e => removeEmbedImages(e));
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (!props.message.content) return;
|
||||
const parsed = await parseMarkdown(props.message.content);
|
||||
await new Promise(e => setTimeout(e, 200));
|
||||
parsedMessage.value = parsed;
|
||||
removeEmbedImages(parsed.body);
|
||||
emits('messageRendered');
|
||||
})
|
||||
|
||||
const AI_MAP: Record<string, { img: string, color: string }> = {
|
||||
GrowthAgent: { img: '/ai/growth.png', color: '#ff861755' },
|
||||
MarketingAgent: { img: '/ai/marketing.png', color: '#bf7fff55' },
|
||||
ProductAgent: { img: '/ai/product.png', color: '#00f33955' },
|
||||
}
|
||||
|
||||
const messageStyle = computed(() => {
|
||||
if (!props.message.name) return;
|
||||
const target = AI_MAP[props.message.name];
|
||||
if (!target) return '';
|
||||
return `background-color: ${target.color};`
|
||||
});
|
||||
|
||||
const isContentMessage = computed(() => !props.message.tool_calls && props.message.content && !hidden.value);
|
||||
const isHiddenMessage = computed(() => !props.message.tool_calls && props.message.content && hidden.value);
|
||||
const isToolMessage = computed(() => props.message.tool_calls);
|
||||
|
||||
function downvoteMessage() {
|
||||
emits('downvoted', props.message_index)
|
||||
hidden.value = true;
|
||||
}
|
||||
|
||||
</script>
|
||||
<template>
|
||||
|
||||
<div class="w-full flex justify-start ml-4">
|
||||
|
||||
|
||||
<div v-if="isToolMessage" class="flex flex-col w-[70%] gap-3">
|
||||
<div class="flex flex-col gap-2 flex-end">
|
||||
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger class="w-fit">
|
||||
<div class="flex gap-1 items-center text-sm w-fit">
|
||||
<InfoIcon class="size-4"></InfoIcon>
|
||||
<div> The ai will use some functions </div>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div class="font-semibold" v-for="tool of message.tool_calls">
|
||||
{{ tool.function.name }}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
</div>
|
||||
|
||||
<div v-if="isToolMessage && message.tool_calls?.[0].function.name === 'createChart'"
|
||||
class="flex flex-col gap-2 flex-end">
|
||||
<AiChart :data="JSON.parse(message.tool_calls[0].function.arguments)"></AiChart>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<div v-if="isContentMessage" :style="messageStyle"
|
||||
class="border rounded-md p-2 flex flex-col gap-2 flex-end w-[70%] relative agent-message-with-content border-accent-foreground/20">
|
||||
|
||||
<div class="absolute left-[-1rem] top-[-1rem] rotate-[-15deg]">
|
||||
<img v-if="message.name && AI_MAP[message.name]" class="h-[3rem]" :src="AI_MAP[message.name].img">
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 items-center">
|
||||
<img class="w-5 h-auto" :src="'/ai/pixel-boy.png'">
|
||||
<Label> {{ message.name ?? 'AI' }} </Label>
|
||||
<Label class="text-sm text-muted-foreground" v-if="message.created_at">
|
||||
{{ new Date(message.created_at).toLocaleString() }}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<MDCRenderer class="md-content !text-gray-800 dark:!text-white" v-if="parsedMessage" :body="parsedMessage.body"
|
||||
:data="parsedMessage.data" />
|
||||
<Skeleton v-if="!parsedMessage" class="w-full h-[5rem]"></Skeleton>
|
||||
</div>
|
||||
|
||||
<div v-if="isHiddenMessage" :style="messageStyle"
|
||||
class="border rounded-md p-2 flex flex-col gap-2 flex-end w-[70%] relative">
|
||||
<div class="absolute left-[-1rem] top-[-1rem] rotate-[-15deg]">
|
||||
<img v-if="message.name && AI_MAP[message.name]" class="h-[3rem]" :src="AI_MAP[message.name].img">
|
||||
</div>
|
||||
<div class="flex gap-2 items-center ml-6">
|
||||
<Label> {{ message.name ?? 'AI' }} </Label>
|
||||
<Label class="text-sm text-muted-foreground" v-if="message.created_at">
|
||||
{{ new Date(message.created_at).toLocaleString() }}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
Message deleted becouse downvoted
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isContentMessage" class="flex ml-2 items-end gap-2">
|
||||
<ThumbsDown @click="downvoteMessage()" :class="{ 'text-red-400': message.downvoted }" class="size-4">
|
||||
</ThumbsDown>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.agent-message-with-content .md-content {
|
||||
|
||||
&:deep() {
|
||||
font-family: system-ui, sans-serif;
|
||||
line-height: 1.5;
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-weight: 700;
|
||||
line-height: 1.25;
|
||||
margin: 2rem 0 1rem;
|
||||
scroll-margin-top: 100px;
|
||||
}
|
||||
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
// Paragraphs
|
||||
p {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
// Links
|
||||
a {
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// Lists
|
||||
ul,
|
||||
ol {
|
||||
padding-left: 1.5rem;
|
||||
margin: 1rem 0;
|
||||
|
||||
li {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Blockquote
|
||||
blockquote {
|
||||
margin: 1.5rem 0;
|
||||
padding: 1rem 1.5rem;
|
||||
border-left: 4px solid #ccc;
|
||||
background-color: #f9f9f9;
|
||||
color: #555;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
// Code blocks
|
||||
pre {
|
||||
background: #1e1e1e;
|
||||
color: #dcdcdc;
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
font-family: 'Fira Code', monospace;
|
||||
font-size: 0.9rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #f3f3f3;
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
font-family: 'Fira Code', monospace;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
// Tables
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 1.5rem 0;
|
||||
font-size: 0.95rem;
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 0.75rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #0000006c;
|
||||
}
|
||||
|
||||
tr:nth-child(even) {
|
||||
background-color: #ffffff23;
|
||||
}
|
||||
}
|
||||
|
||||
// Images
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
margin: 1rem 0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
// Horizontal rule
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid #ccc;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
76
dashboard/components/complex/ai/Chart.vue
Normal file
76
dashboard/components/complex/ai/Chart.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<script lang="ts" setup>
|
||||
import type { ChartData, ChartOptions } from 'chart.js';
|
||||
import { useLineChart, LineChart } from 'vue-chart-3';
|
||||
|
||||
export type AiChartData = {
|
||||
labels: string[],
|
||||
title: string,
|
||||
datasets: {
|
||||
chartType: 'line' | 'bar',
|
||||
points: number[],
|
||||
color: string,
|
||||
name: string
|
||||
}[]
|
||||
}
|
||||
|
||||
const props = defineProps<{ data: AiChartData }>();
|
||||
|
||||
const chartColor = useChartColor();
|
||||
|
||||
const chartOptions = shallowRef<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',
|
||||
},
|
||||
beginAtZero: true,
|
||||
},
|
||||
x: {
|
||||
ticks: { display: true },
|
||||
stacked: false,
|
||||
offset: false,
|
||||
grid: {
|
||||
display: true,
|
||||
drawBorder: false,
|
||||
color: '#CCCCCC22',
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
title: { display: false }
|
||||
},
|
||||
});
|
||||
|
||||
const chartData = shallowRef<ChartData<'line' | 'bar'>>({
|
||||
labels: props.data.labels,
|
||||
datasets: props.data.datasets.map(e => {
|
||||
return {
|
||||
label: e.name,
|
||||
data: e.points,
|
||||
borderColor: e.color ?? '#0000CC',
|
||||
type: e.chartType,
|
||||
backgroundColor: [e.color ?? '#0000CC']
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
const { lineChartProps, lineChartRef, update: updateChart } = useLineChart({ chartData: (chartData as any), options: chartOptions });
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LineChart v-if="chartData" ref="lineChartRef" class="w-full h-full" v-bind="lineChartProps"> </LineChart>
|
||||
</template>
|
||||
65
dashboard/components/complex/ai/ChatsList.vue
Normal file
65
dashboard/components/complex/ai/ChatsList.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<script lang="ts" setup>
|
||||
import { AlertCircle, TrashIcon } from 'lucide-vue-next';
|
||||
import type { TAiNewChatSchema } from '~/shared/schema/ai/AiNewChatSchema';
|
||||
|
||||
const props = defineProps<{ chats: TAiNewChatSchema[] }>();
|
||||
|
||||
const emits = defineEmits<{
|
||||
(event: 'selectChat', chat_id: string): void;
|
||||
(event: 'deleteAllChats'): void;
|
||||
(event: 'deleteChat', chat_id: string): void;
|
||||
}>();
|
||||
|
||||
const separatorIndex = props.chats.toReversed().findIndex(e => new Date(e.updated_at).getUTCDay() < new Date().getUTCDay());
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<div class="flex flex-col gap-4 overflow-hidden h-full">
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<Button @click="emits('deleteAllChats')" size="sm" class="w-full" variant="destructive">
|
||||
Delete all
|
||||
</Button>
|
||||
|
||||
<Button @click="emits('selectChat', 'null')" size="sm" class="w-full" variant="secondary">
|
||||
New chat
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2 overflow-y-auto h-full pr-2 pb-[10rem]">
|
||||
|
||||
<div v-for="(chat, index) of chats.toReversed()">
|
||||
|
||||
<div v-if="separatorIndex === index" class="flex flex-col items-center mt-2 mb-2">
|
||||
<Label class="text-muted-foreground"> Older chats </Label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 rounded-md border p-2">
|
||||
<TooltipProvider>
|
||||
<Tooltip :delay-duration="700">
|
||||
<TooltipTrigger class="grow cursor-pointer flex gap-2 items-center"
|
||||
@click="emits('selectChat', chat._id.toString())">
|
||||
<AlertCircle v-if="chat.status === 'ERRORED'" class="size-4 shrink-0 text-orange-300">
|
||||
</AlertCircle>
|
||||
<div class="text-ellipsis line-clamp-1 text-left">
|
||||
{{ chat.title }}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{{ chat.status === 'ERRORED' ? '[ERROR]' : '' }} {{ chat.title }}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<div class="shrink-0 cursor-pointer hover:text-red-400">
|
||||
<TrashIcon @click="emits('deleteChat', chat._id.toString())" class="size-4"></TrashIcon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
163
dashboard/components/complex/ai/EmptyAiChat.vue
Normal file
163
dashboard/components/complex/ai/EmptyAiChat.vue
Normal file
@@ -0,0 +1,163 @@
|
||||
<script lang="ts" setup>
|
||||
import { ArrowUp, Flame, List, MessageSquareText, TriangleAlert } from 'lucide-vue-next'
|
||||
|
||||
const emits = defineEmits<{
|
||||
(event: 'sendprompt', message: string): void;
|
||||
(event: 'open-sheet'): void;
|
||||
}>();
|
||||
|
||||
const prompts = [
|
||||
'What traffic sources brought the most visitors last week?',
|
||||
'Show me the user retention rate for the past month',
|
||||
"How many users visited the website yesterday?",
|
||||
"Did our traffic increase compared to last month?",
|
||||
"Which page had the most views yesterday?",
|
||||
"Did users spend more time on site this week than last?",
|
||||
"Are desktop users staying longer than mobile users?",
|
||||
"Did our top 5 countries change this month?",
|
||||
"How many users visited the website yesterday?",
|
||||
|
||||
]
|
||||
|
||||
const input = ref('')
|
||||
const toggleSet = ref('')
|
||||
function onKeyPress(e: any) {
|
||||
if (e.key === 'Enter') emits('sendprompt', input.value);
|
||||
}
|
||||
|
||||
const checkInput = computed(() => input.value.trim().length > 0)
|
||||
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!input.value.trim()) return
|
||||
console.log('Inviato:', input.value)
|
||||
input.value = ''
|
||||
}
|
||||
|
||||
//Effetto macchina da scrivere desiderato da fratello antonio
|
||||
const baseText = 'Ask me about... '
|
||||
const placeholder_texts = ['your Month over Month growth in visits', 'your top traffic source last week', 'how long visitors stick around', 'how can I help you', 'to turn your visitor data into a bar chart']
|
||||
|
||||
const placeholder = ref('')
|
||||
|
||||
const typingSpeed = 35
|
||||
const pauseAfterTyping = 800
|
||||
const pauseAfterDeleting = 400
|
||||
|
||||
let index = 0
|
||||
let charIndex = 0
|
||||
let isDeleting = false
|
||||
let typingTimeout: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
function startTyping() {
|
||||
const current = placeholder_texts[index]
|
||||
|
||||
placeholder.value = baseText + current.substring(0, charIndex)
|
||||
|
||||
if (!isDeleting) {
|
||||
if (charIndex < current.length) {
|
||||
charIndex++
|
||||
typingTimeout = setTimeout(startTyping, typingSpeed)
|
||||
} else {
|
||||
typingTimeout = setTimeout(() => {
|
||||
isDeleting = true
|
||||
startTyping()
|
||||
}, pauseAfterTyping)
|
||||
}
|
||||
} else {
|
||||
if (charIndex > 0) {
|
||||
charIndex--
|
||||
typingTimeout = setTimeout(startTyping, typingSpeed)
|
||||
} else {
|
||||
isDeleting = false
|
||||
index = (index + 1) % placeholder_texts.length
|
||||
typingTimeout = setTimeout(startTyping, pauseAfterDeleting)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resetTyping() {
|
||||
if (typingTimeout) clearTimeout(typingTimeout)
|
||||
index = 0
|
||||
charIndex = 0
|
||||
isDeleting = false
|
||||
startTyping()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
startTyping()
|
||||
})
|
||||
|
||||
watch(input, (newValue) => {
|
||||
if (newValue === '') {
|
||||
resetTyping()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-dvh flex items-center justify-center poppins">
|
||||
<div class="w-full max-w-2xl space-y-4">
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="text-center mb-4">
|
||||
<h1 class="text-2xl font-medium dark:text-white text-violet-500 tracking-tight">
|
||||
AI Assistant
|
||||
</h1>
|
||||
<p class="text-sm text-gray-400 dark:text-zinc-400 mt-1">
|
||||
A dedicated team of smart AI experts on Marketing, Growth and Product.
|
||||
</p>
|
||||
</div>
|
||||
<!-- <Alert class="border-yellow-500">
|
||||
<TriangleAlert class="size-4 !text-yellow-500"/>
|
||||
<AlertTitle>Our AI is still in development… we know it’s scrappy.</AlertTitle>
|
||||
<AlertDescription>
|
||||
Using it helps us learn what you really need. Got feedback? We’d love to hear it!
|
||||
</AlertDescription>
|
||||
</Alert> -->
|
||||
</div>
|
||||
<!-- Input container -->
|
||||
<div class="relative bg-gray-200 dark:bg-zinc-800 rounded-2xl p-4 shadow-md flex flex-col gap-4">
|
||||
<div
|
||||
class="absolute z-0 border-2 animate-pulse border-violet-500 w-full h-full top-0 left-0 rounded-[14px]">
|
||||
</div>
|
||||
|
||||
<div class="w-full relative z-10">
|
||||
<Input v-model="input" :placeholder="placeholder"
|
||||
class="pl-0 !bg-transparent !border-none shadow-none text-gray-600 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 !outline-none !ring-0"
|
||||
@keypress="onKeyPress" />
|
||||
|
||||
</div>
|
||||
<div class="flex justify-between items-center gap-2 relative z-10">
|
||||
<ToggleGroup type="single" variant="outline" v-model="toggleSet">
|
||||
|
||||
<ToggleGroupItem value="prompts" aria-label="Toggle italic">
|
||||
<span class="text-sm font-normal items-center flex gap-2">
|
||||
<List class="size-4" /> Prompts
|
||||
</span>
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
<div class="flex gap-2">
|
||||
<Button size="icon" @click="emits('open-sheet')" variant="ghost">
|
||||
<MessageSquareText class="size-4" />
|
||||
</Button>
|
||||
<Button size="icon" @click="emits('sendprompt', input)" :disabled="!checkInput">
|
||||
<ArrowUp class="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden transition-all duration-300"
|
||||
:class="toggleSet === 'prompts' ? 'max-h-40 opacity-100 overflow-y-auto' : 'max-h-0 opacity-0'">
|
||||
<div class="rounded-md flex flex-col gap-2">
|
||||
<Button v-for="p of prompts" variant="outline" @click="emits('sendprompt', p)" class="truncate">{{ p
|
||||
}}</Button>
|
||||
<!-- <NuxtLink to="#">
|
||||
<Button variant="link">View complete list</Button>
|
||||
</NuxtLink> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user