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
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { nanoid } from 'nanoid';
|
||||
import type { BaseMessage } from '~/composables/useChat';
|
||||
import type { ModelWithProvider } from '~/composables/useModels';
|
||||
|
||||
@@ -43,7 +44,23 @@ const handleSubmit = async (message: BaseMessage, model: ModelWithProvider | nul
|
||||
children: [],
|
||||
generationId: null,
|
||||
activeChildId: null,
|
||||
attachments: [],
|
||||
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(),
|
||||
@@ -75,6 +92,27 @@ const handleSubmit = async (message: BaseMessage, model: ModelWithProvider | nul
|
||||
|
||||
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>
|
||||
|
||||
|
||||
@@ -108,7 +146,8 @@ const handleSubmit = async (message: BaseMessage, model: ModelWithProvider | nul
|
||||
|
||||
<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"
|
||||
:agent="agent" :providers="providers?.filter(p => p.enabled)" @submit="handleSubmit" />
|
||||
:allow-manual-role="true" :agent="agent" :providers="providers?.filter(p => p.enabled)"
|
||||
@submit="handleSubmit" @add-message="handleAddMessage" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -55,30 +55,32 @@ const changeSystemPrompt = async (e: Event) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-14 flex items-center justify-between px-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4 px-14 w-full h-full">
|
||||
<div class="flex items-center gap-4">
|
||||
<div>
|
||||
<img v-if="agent?.imageUrl" :src="agent.imageUrl" class="w-16 h-16 rounded-full object-cover" />
|
||||
<span class="text-16 i-mynaui-check-hexagon"></span>
|
||||
<div class="h-full flex flex-col overflow-hidden pb-4">
|
||||
<div class="h-14 flex items-center justify-between px-4 shrink-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<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>
|
||||
</div>
|
||||
<input v-model="name" @input="handleNameInput" placeholder="Agent Name..."
|
||||
class="placeholder:text-[var(--text-tertiary)] w-full bg-transparent rounded-none border-b-4 border-b-[var(--color-border)] text-12 p-0"
|
||||
type="text" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 w-full h-full mb-14">
|
||||
<label class="text-sm text-[var(--text-secondary)]">System Message</label>
|
||||
<textarea v-model="systemPrompt" placeholder="You are a helpful assistant."
|
||||
class="p-4 w-full h-full resize-none bg-transparent rounded-lg border border-[var(--color-border)]"
|
||||
@input="changeSystemPrompt"></textarea>
|
||||
|
||||
<div class="flex flex-col gap-4 px-4 md:px-14 w-full flex-1 min-h-0">
|
||||
<div class="flex items-center gap-4">
|
||||
<div>
|
||||
<img v-if="agent?.imageUrl" :src="agent.imageUrl" class="w-16 h-16 rounded-full object-cover" />
|
||||
<span class="text-16 i-mynaui-check-hexagon"></span>
|
||||
</div>
|
||||
<input v-model="name" @input="handleNameInput" placeholder="Agent Name..."
|
||||
class="placeholder:text-[var(--text-tertiary)] w-full bg-transparent rounded-none border-b-4 border-b-[var(--color-border)] text-12 p-0"
|
||||
type="text" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 w-full flex-1 min-h-0">
|
||||
<label class="text-sm text-[var(--text-secondary)]">System Message</label>
|
||||
<textarea v-model="systemPrompt" placeholder="You are a helpful assistant."
|
||||
class="p-4 w-full flex-1 min-h-0 resize-none bg-transparent rounded-lg border border-[var(--color-border)]"
|
||||
@input="changeSystemPrompt"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -54,6 +54,27 @@ const submitMessage = async (message: BaseMessage, model: ModelWithProvider | nu
|
||||
startGeneration(model);
|
||||
};
|
||||
|
||||
const handleAddMessage = async (message: BaseMessage, role: 'user' | 'assistant', _model: ModelWithProvider | null) => {
|
||||
inputValue.value = { content: '', fileIds: [] };
|
||||
|
||||
const res = await sendMessage(message, async () => {
|
||||
await nextTick();
|
||||
setTimeout(() => {
|
||||
scrollToBottom('instant');
|
||||
});
|
||||
}, role);
|
||||
if (!res.ok) {
|
||||
console.error('Failed to add message:', res.error);
|
||||
const chatInput = document.getElementById('chat') as HTMLInputElement | null;
|
||||
if (chatInput) {
|
||||
inputValue.value = message;
|
||||
nextTick(() => {
|
||||
chatInput.focus();
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleRegenerate = async (message: Message) => {
|
||||
if (!agent.value!.defaultModelId) {
|
||||
console.error('No model selected');
|
||||
@@ -292,8 +313,8 @@ console.log("full page render took", Date.now() - rootStart);
|
||||
|
||||
<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"
|
||||
:loading="activeGeneration !== null" :agent="agent"
|
||||
:providers="providers?.filter(p => p.enabled)" @submit="submitMessage" @cancel="handleCancel"
|
||||
:loading="activeGeneration !== null" :allow-manual-role="true" :agent="agent"
|
||||
:providers="providers?.filter(p => p.enabled)" @submit="submitMessage" @add-message="handleAddMessage" @cancel="handleCancel"
|
||||
@resize="handleResize" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user