Files
veridian/app/pages/agent/[id]/index.vue
T
zoeissleeping 8ccaa824dd feat: file upload retry, secure file tokens, UI polish
- Add HMAC-based file token auth for secure AI model file access
- Add file upload retry with exponential backoff (max 3 retries)
- File endpoint now requires session auth or signed token
- Support assistant role messages in chat input
- Optimistic UI for attachments on message send
- Verify topic ownership before allowing messages
- Switch web scraping to Firecrawl API
- Agent profile page layout fixes (proper flex overflow)
- Add quick switcher (Ctrl+K) to sidenav
- Clean up longcat.ts and stale comments
2026-06-06 00:16:39 -05:00

156 lines
5.3 KiB
Vue

<script setup lang="ts">
import { nanoid } from 'nanoid';
import type { BaseMessage } from '~/composables/useChat';
import type { ModelWithProvider } from '~/composables/useModels';
const route = useRoute();
const inputValue = ref<BaseMessage>({ content: '', fileIds: [] });
const pendingMessage = ref<Message | null>(null);
const { open: sidebarOpen, openSidebar } = useSidebar();
const { autoRename: autoRenameTopic } = useTopic();
const { getAgent, createTopic } = await useAgents();
const { providers } = await useModels();
const agent = getAgent(route.params.id as string);
if (!agent.value) navigateTo('/');
useHead({ title: `${agent.value!.name} | Veridian` });
const handleSubmit = async (message: BaseMessage, model: ModelWithProvider | null) => {
if (!model) {
console.error('No model selected');
return;
}
const user = useAuth().user;
if (!user) {
console.error('No user');
return;
}
console.log("handleSubmit", message);
pendingMessage.value = {
id: '',
userId: user.value!.id,
topicId: '',
content: message.content,
role: 'user',
parts: [],
generation: null,
parentMessageId: null,
children: [],
generationId: null,
activeChildId: null,
attachments: message.files?.map(file => ({
id: nanoid(),
userId: user.value!.id,
topicId: '',
messageId: '',
fileId: file.id,
createdAt: new Date(),
file: {
id: file.id,
userId: user.value!.id,
name: file.name,
mimeType: file.mimeType,
size: file.size ?? 0,
url: file.url,
createdAt: new Date(),
},
})) ?? [],
deleted: false,
createdAt: new Date(),
updatedAt: new Date(),
}
const topic = await createTopic(agent.value!.id);
if (!topic) throw new Error('Failed to create topic');
const { sendMessage, startGeneration } = await useChat(topic.id, false);
const res = await sendMessage(message);
if (res.ok === false) {
console.error('Failed to send message:', res.error);
pendingMessage.value = null;
await navigateTo(`/agent/${route.params.id}`);
nextTick(() => {
inputValue.value = message;
});
return;
}
await navigateTo(`/agent/${route.params.id}/topic/${topic.id}`);
autoRenameTopic(topic.id);
return startGeneration(model, undefined, route.params.id as string);
};
const handleAddMessage = async (message: BaseMessage, role: 'user' | 'assistant', _model: ModelWithProvider | null) => {
const user = useAuth().user;
if (!user) {
console.error('No user');
return;
}
const topic = await createTopic(agent.value!.id);
if (!topic) throw new Error('Failed to create topic');
const { sendMessage } = await useChat(topic.id, false);
const res = await sendMessage(message, undefined, role);
if (res.ok === false) {
console.error('Failed to send message:', res.error);
return;
}
await navigateTo(`/agent/${route.params.id}/topic/${topic.id}`);
};
</script>
<template>
<div class="flex flex-col h-full w-full max-w-full overflow-x-hidden">
<div class="h-14 flex items-center justify-between px-4 border-b border-[var(--color-border)]">
<div class="flex items-center gap-2 max-w-full">
<button v-if="!sidebarOpen" @click="openSidebar"
class="flex p-2 rounded-lg text-[var(--text-primary)] bg-transparent @hover:bg-[var(--color-hover)] transition-colors">
<span class="i-mynaui-panel-left-open text-5"></span>
</button>
<h4 class="text-lg truncate">
{{ agent?.name }}
</h4>
</div>
</div>
<div ref="chatPaneWrapper"
class="chat-scroll-container justify-center w-full h-full [scrollbar-width:thin] [scrollbar-color:#888_transparent] overflow-y-scroll overflow-x-hidden flex justify-center pt-4">
<div class="chatPane max-w-4xl w-full min-h-full flex flex-col px-4">
<div class="w-full px-px flex flex-col flex-grow gap-2"
:class="pendingMessage === null ? 'justify-end pb-28' : 'pb-9'">
<div v-if="pendingMessage === null">
<h1 v-if="agent" class="font-bold">{{ agent.name }}</h1>
<p class="text-[var(--text-secondary)]">Select a topic to continue or create a new one</p>
</div>
<div v-else class="opacity-70">
<Message :message="pendingMessage" />
</div>
</div>
<div class="sticky bottom-0 z-10 bg-[var(--bg-surface)] pb-4 w-full rounded-t-2xl">
<ChatInput v-model="inputValue" class="[view-transition-name:chat-prompt] duration-150 ease-in-out"
:allow-manual-role="true" :agent="agent" :providers="providers?.filter(p => p.enabled)"
@submit="handleSubmit" @add-message="handleAddMessage" />
</div>
</div>
</div>
</div>
</template>