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:
Zoe
2026-06-06 00:16:39 -05:00
parent 47009b1f0a
commit 8ccaa824dd
20 changed files with 827 additions and 406 deletions
+88 -26
View File
@@ -6,6 +6,13 @@ import { buildMessageTree } from '~~/utils/message';
export type BaseMessage = {
content: string;
fileIds: string[];
files?: {
id: string;
name: string;
mimeType: string;
url: string;
size?: number;
}[];
}
export type ToolCall = typeof schema.toolCalls.$inferSelect
@@ -263,12 +270,6 @@ export const useChat = async (topicId: MaybeRef<string>, connect = true) => {
const { data: ssrData } = await useFetch<Topic & { messages: Message[] }>(`/api/topic/${id}`);
data.value = ssrData.value;
} else {
// if (data.value === undefined) {
// const id = unref(topicId);
// const { data: ssrData } = await useFetch<Topic & { messages: Message[] }>(`/api/topic/${id}`);
// data.value = ssrData.value;
// }
await connectSSE();
}
}
@@ -305,6 +306,7 @@ export const useChat = async (topicId: MaybeRef<string>, connect = true) => {
const sendMessage = async (
baseMessage: BaseMessage,
onRequest?: () => void,
role: 'user' | 'assistant' = 'user',
): Promise<Result<void, ChatErrorType>> => {
const { user } = useAuth();
if (!user.value) return Err(ChatErrorType.NoUser);
@@ -312,34 +314,94 @@ export const useChat = async (topicId: MaybeRef<string>, connect = true) => {
try {
const message = {
id: nanoid(),
role: 'user',
content: baseMessage.content,
fileIds: baseMessage.fileIds,
role,
}
await $fetch(`/api/topic/${unref(topicId)}/message`, {
method: 'POST',
body: {
message
message: {
...message,
content: baseMessage.content,
fileIds: baseMessage.fileIds,
}
},
onRequest() {
if (data.value) {
data.value!.messages.push({
topicId: unref(topicId),
userId: user.value!.id,
// TODO
attachments: [],
parts: undefined,
generation: null,
parentMessageId: null,
generationId: null,
activeChildId: null,
deleted: null,
createdAt: new Date(),
updatedAt: new Date(),
children: [],
...message
} as Message);
switch (role) {
case 'user': {
const optimisticAttachments = baseMessage.files?.map(file => ({
id: nanoid(),
userId: user.value!.id,
topicId: unref(topicId),
messageId: message.id,
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(),
},
})) ?? [];
data.value!.messages.push({
topicId: unref(topicId),
userId: user.value!.id,
attachments: optimisticAttachments,
parts: undefined,
generation: null,
parentMessageId: null,
generationId: null,
activeChildId: null,
children: [],
deleted: false,
content: baseMessage.content,
createdAt: new Date(),
updatedAt: new Date(),
...message
});
break;
}
case 'assistant': {
data.value!.messages.push({
topicId: unref(topicId),
userId: user.value!.id,
// TODO
attachments: [],
generation: null,
parentMessageId: null,
generationId: null,
activeChildId: null,
children: [],
deleted: false,
content: null,
createdAt: new Date(),
updatedAt: new Date(),
parts: [
{
id: 'bogus-id',
userId: user.value!.id,
topicId: unref(topicId),
messageId: message.id,
type: 'text',
content: baseMessage.content,
finished: true,
createdAt: new Date(),
lastUpdatedAt: new Date(),
providerOptions: null,
toolCallId: null,
toolCall: null,
}
],
...message
});
break;
}
}
}
onRequest?.();
},