mirror of
https://github.com/ArthurDanjou/artagents.git
synced 2026-02-01 04:37:57 +01:00
feat: add chat and file management APIs, implement chat loading and saving functionality
- Introduced new API endpoints for chat management including posting and retrieving chat messages. - Implemented file upload and deletion functionalities for chat and other files. - Added utility functions for streaming text and loading chat data. - Created TypeScript types for models and agents used in the application. - Configured TypeScript settings for server and project. - Added favicon and workspace configuration for pnpm.
This commit is contained in:
302
app/pages/[agent].vue
Normal file
302
app/pages/[agent].vue
Normal file
@@ -0,0 +1,302 @@
|
||||
<script setup lang="ts">
|
||||
import { useChat } from '@ai-sdk/vue'
|
||||
import { onStartTyping } from '@vueuse/core'
|
||||
import type { PROVIDERS } from '~~/types'
|
||||
import { AGENTS, MODELS } from '~~/types'
|
||||
|
||||
const toast = useToast()
|
||||
const { agent } = useRoute().params
|
||||
const currentAgent = AGENTS.find(item => item.slug === agent)
|
||||
|
||||
if (!currentAgent) {
|
||||
toast.clear()
|
||||
toast.add({
|
||||
title: 'Agent not found',
|
||||
description: `Please try again`,
|
||||
color: 'error'
|
||||
})
|
||||
}
|
||||
|
||||
const model = ref<PROVIDERS>(currentAgent!.defaultModel)
|
||||
const selectedModel = computed(() => MODELS.find(item => item.model === model.value))
|
||||
|
||||
const initialMessages = await loadChat(currentAgent!.slug)
|
||||
const { messages, input, handleSubmit, status, stop, error, reload } = useChat({
|
||||
id: currentAgent?.slug,
|
||||
initialMessages,
|
||||
sendExtraMessageFields: true,
|
||||
experimental_prepareRequestBody({ messages, id }) {
|
||||
return { message: messages[messages.length - 1], id }
|
||||
},
|
||||
body: {
|
||||
model: model.value,
|
||||
agent: currentAgent
|
||||
}
|
||||
})
|
||||
|
||||
const inputRef = shallowRef<HTMLInputElement | null>(null)
|
||||
onStartTyping(() => { // TODO: fix focus
|
||||
if (inputRef.value !== document.activeElement)
|
||||
inputRef.value!.focus()
|
||||
})
|
||||
|
||||
const messagesRef = ref<HTMLDivElement | null>(null)
|
||||
onMounted(() => {
|
||||
if (messagesRef.value) {
|
||||
messagesRef.value.scrollTo({ top: messagesRef.value.scrollHeight, behavior: 'smooth' })
|
||||
}
|
||||
})
|
||||
|
||||
const isModalOpen = ref(false)
|
||||
async function deleteConversation() {
|
||||
await deleteChat(currentAgent!.slug)
|
||||
window.location.reload()
|
||||
isModalOpen.value = false
|
||||
toast.add({
|
||||
title: 'Conversation deleted',
|
||||
description: `The conversation has been deleted`,
|
||||
color: 'success'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main
|
||||
v-if="currentAgent"
|
||||
class="flex flex-col justify-between"
|
||||
>
|
||||
<div
|
||||
class="flex justify-between w-full items-center"
|
||||
>
|
||||
<UButton
|
||||
icon="i-heroicons-chevron-left"
|
||||
color="primary"
|
||||
variant="ghost"
|
||||
class="rounded-full"
|
||||
@click="$router.push({ path: '/' })"
|
||||
/>
|
||||
<div class="flex flex-col items-center justify-center mb-4">
|
||||
<div class="flex items-center justify-center bg-neutral-300 dark:bg-neutral-800 rounded-full p-2">
|
||||
<UIcon
|
||||
:name="currentAgent.icon"
|
||||
size="32"
|
||||
/>
|
||||
</div>
|
||||
<p class="text-2xl">
|
||||
{{ currentAgent.name }}
|
||||
</p>
|
||||
</div>
|
||||
<UPopover
|
||||
:content="{
|
||||
align: 'end',
|
||||
side: 'bottom'
|
||||
}"
|
||||
>
|
||||
<UButton
|
||||
icon="i-heroicons-ellipsis-horizontal"
|
||||
color="primary"
|
||||
variant="ghost"
|
||||
class="rounded-full"
|
||||
/>
|
||||
<template #content>
|
||||
<div class="p-2 space-y-2 max-w-94 min-w-48">
|
||||
<div class="flex items-start gap-2">
|
||||
<p class="text-sm font-bold">
|
||||
Name
|
||||
</p>
|
||||
<p class="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
{{ currentAgent.name }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-start gap-2">
|
||||
<p class="text-sm font-bold">
|
||||
Description
|
||||
</p>
|
||||
<p class="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
{{ currentAgent.description }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-start gap-2">
|
||||
<p class="text-sm font-bold">
|
||||
Model
|
||||
</p>
|
||||
<p class="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
{{ currentAgent.defaultModel }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-start gap-4">
|
||||
<p class="text-sm font-bold">
|
||||
Prompt
|
||||
</p>
|
||||
<p class="text-sm text-neutral-600 dark:text-neutral-400 text-justify">
|
||||
{{ currentAgent.prompt }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</UPopover>
|
||||
</div>
|
||||
<div
|
||||
ref="messagesRef"
|
||||
class="h-full overflow-auto"
|
||||
>
|
||||
<div v-if="initialMessages.length > 0">
|
||||
<div
|
||||
v-for="message in initialMessages"
|
||||
:key="message.id"
|
||||
class="flex flex-col py-4"
|
||||
>
|
||||
<div :class="message.role === 'assistant' ? 'pr-8 mr-auto' : 'pl-8 ml-auto'">
|
||||
<MDC
|
||||
class="p-2 mt-1 text-sm rounded-xl text-smp-2 whitespace-pre-line"
|
||||
:class="message.role === 'assistant' ? 'text-gray-700 bg-neutral-100 dark:text-white dark:bg-neutral-800' : 'text-white bg-blue-400 dark:text-white dark:bg-blue-400'"
|
||||
:value="message.content"
|
||||
:cache-key="message.id.toString()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<USeparator
|
||||
label="New conversation"
|
||||
color="neutral"
|
||||
type="dotted"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div class="pr-8 mr-auto py-4">
|
||||
<MDC
|
||||
class="p-2 mt-1 text-sm rounded-xl text-smp-2 whitespace-pre-line text-gray-700 bg-neutral-100 dark:text-white dark:bg-neutral-800"
|
||||
:value="currentAgent?.welcomeMessage || 'Hello, how can I help you today?'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-for="message in messages.filter(m => !initialMessages.some(im => im.id === m.id))"
|
||||
:key="message.id"
|
||||
class="flex flex-col py-4"
|
||||
>
|
||||
<div :class="message.role === 'assistant' ? 'pr-8 mr-auto' : 'pl-8 ml-auto'">
|
||||
<MDC
|
||||
class="p-2 mt-1 text-sm rounded-xl text-smp-2 whitespace-pre-line"
|
||||
:class="message.role === 'assistant' ? 'text-gray-700 bg-neutral-100 dark:text-white dark:bg-neutral-800' : 'text-white bg-blue-400 dark:text-white dark:bg-blue-400'"
|
||||
:value="message.content"
|
||||
:cache-key="message.id"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="error"
|
||||
class="flex items-center justify-center gap-2"
|
||||
>
|
||||
<div class="text-red-500">
|
||||
{{ 'An error occurred' }}
|
||||
</div>
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="subtle"
|
||||
size="xs"
|
||||
@click="reload()"
|
||||
>
|
||||
retry
|
||||
</UButton>
|
||||
</div>
|
||||
<ClientOnly>
|
||||
<form
|
||||
class="mt-4 shadow-md flex flex-col items-center w-full p-2 gap-2 bg-zinc-100 rounded-2xl dark:bg-neutral-800"
|
||||
@submit.prevent="handleSubmit"
|
||||
@keydown.enter.prevent="handleSubmit"
|
||||
>
|
||||
<div class="w-full flex gap-2">
|
||||
<UTextarea
|
||||
ref="inputRef"
|
||||
v-model="input"
|
||||
placeholder="Type what you want to ask..."
|
||||
class="w-full min-h-8"
|
||||
:ui="{
|
||||
base: 'min-h-8'
|
||||
}"
|
||||
color="primary"
|
||||
autoresize
|
||||
:rows="1"
|
||||
variant="soft"
|
||||
:disabled="Boolean(error)"
|
||||
/>
|
||||
<div class="flex items-start">
|
||||
<UButton
|
||||
v-if="status !== 'ready'"
|
||||
icon="i-heroicons-stop"
|
||||
color="primary"
|
||||
@click="stop"
|
||||
/>
|
||||
<UButton
|
||||
v-else
|
||||
icon="i-heroicons-arrow-long-up-16-solid"
|
||||
type="submit"
|
||||
class="rounded-xl duration-300"
|
||||
:color="input.length === 0 ? 'neutral' : 'primary'"
|
||||
:disabled="input.length === 0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full flex items-center gap-2 px-2">
|
||||
<UButton
|
||||
icon="i-heroicons-paper-clip"
|
||||
color="neutral"
|
||||
variant="subtle"
|
||||
class="rounded-full"
|
||||
@click="stop"
|
||||
/>
|
||||
<UTooltip text="Select model">
|
||||
<USelect
|
||||
v-model="model"
|
||||
class="rounded-full w-40"
|
||||
color="neutral"
|
||||
variant="subtle"
|
||||
:items="MODELS"
|
||||
label-key="name"
|
||||
value-key="model"
|
||||
:icon="selectedModel?.icon"
|
||||
trailing-icon=""
|
||||
leading-icon=""
|
||||
/>
|
||||
</UTooltip>
|
||||
<UModal
|
||||
v-model:open="isModalOpen"
|
||||
title="Are you sure you want to delete this conversation?"
|
||||
description="This action cannot be undone. The agent will lost all the conversation history and context."
|
||||
:ui="{ footer: 'justify-end' }"
|
||||
>
|
||||
<UTooltip text="Clear conversation">
|
||||
<UButton
|
||||
:disabled="messages.length === 0"
|
||||
icon="i-heroicons-trash"
|
||||
color="neutral"
|
||||
variant="subtle"
|
||||
class="rounded-full"
|
||||
/>
|
||||
</UTooltip>
|
||||
<template #footer>
|
||||
<div class="flex gap-2">
|
||||
<UButton
|
||||
color="neutral"
|
||||
label="Dismiss"
|
||||
@click.prevent="isModalOpen = false"
|
||||
/>
|
||||
<UButton
|
||||
color="error"
|
||||
label="Delete"
|
||||
@click.prevent="deleteConversation"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</UModal>
|
||||
</div>
|
||||
</form>
|
||||
</ClientOnly>
|
||||
</main>
|
||||
<main v-else>
|
||||
<div class="flex items-center justify-center h-full">
|
||||
Agent not found
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
68
app/pages/index.vue
Normal file
68
app/pages/index.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<script setup lang="ts">
|
||||
import { AGENTS, MODELS } from '~~/types'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="flex flex-col gap-16">
|
||||
<div class="flex flex-col gap-4">
|
||||
<h1 class="text-2xl font-bold">
|
||||
Choose an agent
|
||||
</h1>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div
|
||||
v-for="agent in AGENTS"
|
||||
:key="agent.slug"
|
||||
>
|
||||
<NuxtLink
|
||||
:to="`/${agent.slug}`"
|
||||
class="flex items-start gap-2 hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-lg p-4 duration-300"
|
||||
>
|
||||
<div class="flex items-center justify-center bg-neutral-300 dark:bg-neutral-800 rounded-full p-2">
|
||||
<UIcon
|
||||
:name="agent.icon"
|
||||
size="24"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<h2 class="text-lg font-bold">
|
||||
{{ agent.name }}
|
||||
</h2>
|
||||
<p>
|
||||
{{ agent.description }}
|
||||
</p>
|
||||
<p>
|
||||
{{ agent.defaultModel }}
|
||||
</p>
|
||||
<p>
|
||||
{{ agent.prompt }}
|
||||
</p>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-4">
|
||||
<h1 class="text-2xl font-bold">
|
||||
Available models
|
||||
</h1>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-8">
|
||||
<div
|
||||
v-for="model in MODELS"
|
||||
:key="model.name"
|
||||
>
|
||||
<div class="flex gap-4 items-center">
|
||||
<div class="flex items-center justify-center bg-neutral-300 dark:bg-neutral-800 rounded-full p-2">
|
||||
<UIcon
|
||||
:name="model.icon"
|
||||
size="24"
|
||||
/>
|
||||
</div>
|
||||
<p>
|
||||
{{ model.name }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
Reference in New Issue
Block a user