Shared conversation redirect (#1968)
Browse files* redirect to the convo
* backend redirect to the convo
* fix: fetch url logs
* server side redirect
* require a query only for the get
* fix error type
* fix: fetch url logs
---------
Co-authored-by: rtrm <remy.trompier@gmail.com>
- src/lib/components/BackgroundGenerationPoller.svelte +1 -1
- src/lib/server/api/routes/groups/conversations.ts +28 -16
- src/lib/server/conversation.ts +54 -0
- src/routes/conversation/[id]/+page.server.ts +21 -0
- src/routes/conversation/[id]/+page.svelte +7 -61
- src/routes/conversation/[id]/+page.ts +4 -1
- src/routes/r/[id]/+page.server.ts +23 -0
- src/routes/r/[id]/+page.ts +0 -7
src/lib/components/BackgroundGenerationPoller.svelte
CHANGED
|
@@ -64,7 +64,7 @@
|
|
| 64 |
log("poll", id);
|
| 65 |
|
| 66 |
try {
|
| 67 |
-
const response = await client.conversations({ id }).get();
|
| 68 |
const conversation = handleResponse(response);
|
| 69 |
const messages = conversation?.messages ?? [];
|
| 70 |
const lastAssistant = [...messages]
|
|
|
|
| 64 |
log("poll", id);
|
| 65 |
|
| 66 |
try {
|
| 67 |
+
const response = await client.conversations({ id }).get({ query: {} });
|
| 68 |
const conversation = handleResponse(response);
|
| 69 |
const messages = conversation?.messages ?? [];
|
| 70 |
const lastAssistant = [...messages]
|
src/lib/server/api/routes/groups/conversations.ts
CHANGED
|
@@ -72,18 +72,17 @@ export const conversationGroup = new Elysia().use(authPlugin).group("/conversati
|
|
| 72 |
},
|
| 73 |
(app) => {
|
| 74 |
return app
|
| 75 |
-
.derive(async ({ locals, params }) => {
|
| 76 |
let conversation;
|
| 77 |
let shared = false;
|
| 78 |
|
| 79 |
-
// if the
|
| 80 |
if (params.id.length === 7) {
|
| 81 |
// shared link of length 7
|
| 82 |
conversation = await collections.sharedConversations.findOne({
|
| 83 |
_id: params.id,
|
| 84 |
});
|
| 85 |
shared = true;
|
| 86 |
-
|
| 87 |
if (!conversation) {
|
| 88 |
throw new Error("Conversation not found");
|
| 89 |
}
|
|
@@ -113,6 +112,9 @@ export const conversationGroup = new Elysia().use(authPlugin).group("/conversati
|
|
| 113 |
|
| 114 |
throw new Error("Conversation not found.");
|
| 115 |
}
|
|
|
|
|
|
|
|
|
|
| 116 |
}
|
| 117 |
|
| 118 |
const convertedConv = {
|
|
@@ -123,19 +125,29 @@ export const conversationGroup = new Elysia().use(authPlugin).group("/conversati
|
|
| 123 |
|
| 124 |
return { conversation: convertedConv };
|
| 125 |
})
|
| 126 |
-
.get(
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
.post("", () => {
|
| 140 |
// todo: post new message
|
| 141 |
throw new Error("Not implemented");
|
|
|
|
| 72 |
},
|
| 73 |
(app) => {
|
| 74 |
return app
|
| 75 |
+
.derive(async ({ locals, params, query }) => {
|
| 76 |
let conversation;
|
| 77 |
let shared = false;
|
| 78 |
|
| 79 |
+
// if the conversation is shared
|
| 80 |
if (params.id.length === 7) {
|
| 81 |
// shared link of length 7
|
| 82 |
conversation = await collections.sharedConversations.findOne({
|
| 83 |
_id: params.id,
|
| 84 |
});
|
| 85 |
shared = true;
|
|
|
|
| 86 |
if (!conversation) {
|
| 87 |
throw new Error("Conversation not found");
|
| 88 |
}
|
|
|
|
| 112 |
|
| 113 |
throw new Error("Conversation not found.");
|
| 114 |
}
|
| 115 |
+
if (query.fromShare && conversation.meta?.fromShareId === query.fromShare) {
|
| 116 |
+
shared = true;
|
| 117 |
+
}
|
| 118 |
}
|
| 119 |
|
| 120 |
const convertedConv = {
|
|
|
|
| 125 |
|
| 126 |
return { conversation: convertedConv };
|
| 127 |
})
|
| 128 |
+
.get(
|
| 129 |
+
"",
|
| 130 |
+
async ({ conversation }) => {
|
| 131 |
+
return {
|
| 132 |
+
messages: conversation.messages,
|
| 133 |
+
title: conversation.title,
|
| 134 |
+
model: conversation.model,
|
| 135 |
+
preprompt: conversation.preprompt,
|
| 136 |
+
rootMessageId: conversation.rootMessageId,
|
| 137 |
+
id: conversation._id.toString(),
|
| 138 |
+
updatedAt: conversation.updatedAt,
|
| 139 |
+
modelId: conversation.model,
|
| 140 |
+
shared: conversation.shared,
|
| 141 |
+
};
|
| 142 |
+
},
|
| 143 |
+
{
|
| 144 |
+
query: t.Optional(
|
| 145 |
+
t.Object({
|
| 146 |
+
fromShare: t.Optional(t.String()),
|
| 147 |
+
})
|
| 148 |
+
),
|
| 149 |
+
}
|
| 150 |
+
)
|
| 151 |
.post("", () => {
|
| 152 |
// todo: post new message
|
| 153 |
throw new Error("Not implemented");
|
src/lib/server/conversation.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { collections } from "$lib/server/database";
|
| 2 |
+
import { MetricsServer } from "$lib/server/metrics";
|
| 3 |
+
import { error } from "@sveltejs/kit";
|
| 4 |
+
import { ObjectId } from "mongodb";
|
| 5 |
+
import { authCondition } from "$lib/server/auth";
|
| 6 |
+
|
| 7 |
+
/**
|
| 8 |
+
* Create a new conversation from a shared conversation ID.
|
| 9 |
+
* If the conversation already exists for the user/session, return the existing conversation ID.
|
| 10 |
+
* returns the conversation ID.
|
| 11 |
+
*/
|
| 12 |
+
export async function createConversationFromShare(
|
| 13 |
+
fromShareId: string,
|
| 14 |
+
locals: App.Locals,
|
| 15 |
+
userAgent?: string
|
| 16 |
+
): Promise<string> {
|
| 17 |
+
const conversation = await collections.sharedConversations.findOne({
|
| 18 |
+
_id: fromShareId,
|
| 19 |
+
});
|
| 20 |
+
|
| 21 |
+
if (!conversation) {
|
| 22 |
+
error(404, "Conversation not found");
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
// Check if shared conversation exists already for this user/session
|
| 26 |
+
const existingConversation = await collections.conversations.findOne({
|
| 27 |
+
"meta.fromShareId": fromShareId,
|
| 28 |
+
...authCondition(locals),
|
| 29 |
+
});
|
| 30 |
+
|
| 31 |
+
if (existingConversation) {
|
| 32 |
+
return existingConversation._id.toString();
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
// Create new conversation from shared conversation
|
| 36 |
+
const res = await collections.conversations.insertOne({
|
| 37 |
+
_id: new ObjectId(),
|
| 38 |
+
title: conversation.title.replace(/<\/?think>/gi, "").trim(),
|
| 39 |
+
rootMessageId: conversation.rootMessageId,
|
| 40 |
+
messages: conversation.messages,
|
| 41 |
+
model: conversation.model,
|
| 42 |
+
preprompt: conversation.preprompt,
|
| 43 |
+
createdAt: new Date(),
|
| 44 |
+
updatedAt: new Date(),
|
| 45 |
+
userAgent,
|
| 46 |
+
...(locals.user ? { userId: locals.user._id } : { sessionId: locals.sessionId }),
|
| 47 |
+
meta: { fromShareId },
|
| 48 |
+
});
|
| 49 |
+
|
| 50 |
+
if (MetricsServer.isEnabled()) {
|
| 51 |
+
MetricsServer.getMetrics().model.conversationsTotal.inc({ model: conversation.model });
|
| 52 |
+
}
|
| 53 |
+
return res.insertedId.toString();
|
| 54 |
+
}
|
src/routes/conversation/[id]/+page.server.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { redirect } from "@sveltejs/kit";
|
| 2 |
+
import type { PageServerLoad } from "./$types";
|
| 3 |
+
import { loginEnabled } from "$lib/server/auth";
|
| 4 |
+
import { createConversationFromShare } from "$lib/server/conversation";
|
| 5 |
+
|
| 6 |
+
export const load: PageServerLoad = async ({ url, params, locals, request }) => {
|
| 7 |
+
/// if logged in and valid share ID, create conversation from share, usually occurs after login
|
| 8 |
+
if (loginEnabled && locals.user && params.id.length === 7) {
|
| 9 |
+
const leafId = url.searchParams.get("leafId");
|
| 10 |
+
const conversationId = await createConversationFromShare(
|
| 11 |
+
params.id,
|
| 12 |
+
locals,
|
| 13 |
+
request.headers.get("User-Agent") ?? undefined
|
| 14 |
+
);
|
| 15 |
+
|
| 16 |
+
return redirect(
|
| 17 |
+
302,
|
| 18 |
+
`../conversation/${conversationId}?leafId=${leafId}&fromShare=${params.id}`
|
| 19 |
+
);
|
| 20 |
+
}
|
| 21 |
+
};
|
src/routes/conversation/[id]/+page.svelte
CHANGED
|
@@ -4,7 +4,7 @@
|
|
| 4 |
import { isAborted } from "$lib/stores/isAborted";
|
| 5 |
import { onMount } from "svelte";
|
| 6 |
import { page } from "$app/state";
|
| 7 |
-
import { beforeNavigate,
|
| 8 |
import { base } from "$app/paths";
|
| 9 |
import { ERROR_MESSAGES, error } from "$lib/stores/errors";
|
| 10 |
import { findCurrentModel } from "$lib/utils/models";
|
|
@@ -95,35 +95,6 @@
|
|
| 95 |
return alternatives;
|
| 96 |
}
|
| 97 |
|
| 98 |
-
async function convFromShared() {
|
| 99 |
-
try {
|
| 100 |
-
$loading = true;
|
| 101 |
-
const res = await fetch(`${base}/conversation`, {
|
| 102 |
-
method: "POST",
|
| 103 |
-
headers: {
|
| 104 |
-
"Content-Type": "application/json",
|
| 105 |
-
},
|
| 106 |
-
body: JSON.stringify({
|
| 107 |
-
fromShare: page.params.id,
|
| 108 |
-
model: data.model,
|
| 109 |
-
}),
|
| 110 |
-
});
|
| 111 |
-
|
| 112 |
-
if (!res.ok) {
|
| 113 |
-
error.set(await res.text());
|
| 114 |
-
console.error("Error while creating conversation: " + (await res.text()));
|
| 115 |
-
return;
|
| 116 |
-
}
|
| 117 |
-
|
| 118 |
-
const { conversationId } = await res.json();
|
| 119 |
-
|
| 120 |
-
return conversationId;
|
| 121 |
-
} catch (err) {
|
| 122 |
-
error.set(ERROR_MESSAGES.default);
|
| 123 |
-
console.error(String(err));
|
| 124 |
-
throw err;
|
| 125 |
-
}
|
| 126 |
-
}
|
| 127 |
// this function is used to send new message to the backends
|
| 128 |
async function writeMessage({
|
| 129 |
prompt,
|
|
@@ -381,16 +352,7 @@
|
|
| 381 |
});
|
| 382 |
|
| 383 |
async function onMessage(content: string) {
|
| 384 |
-
|
| 385 |
-
await writeMessage({ prompt: content });
|
| 386 |
-
} else {
|
| 387 |
-
await convFromShared()
|
| 388 |
-
.then(async (convId) => {
|
| 389 |
-
await goto(`${base}/conversation/${convId}`, { invalidateAll: true });
|
| 390 |
-
})
|
| 391 |
-
.then(async () => await writeMessage({ prompt: content }))
|
| 392 |
-
.finally(() => ($loading = false));
|
| 393 |
-
}
|
| 394 |
}
|
| 395 |
|
| 396 |
async function onRetry(payload: { id: Message["id"]; content?: string }) {
|
|
@@ -399,27 +361,11 @@
|
|
| 399 |
const lastMsgId = payload.id;
|
| 400 |
messagesPath = createMessagesPath(messages, lastMsgId);
|
| 401 |
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
});
|
| 408 |
-
} else {
|
| 409 |
-
await convFromShared()
|
| 410 |
-
.then(async (convId) => {
|
| 411 |
-
await goto(`${base}/conversation/${convId}`, { invalidateAll: true });
|
| 412 |
-
})
|
| 413 |
-
.then(
|
| 414 |
-
async () =>
|
| 415 |
-
await writeMessage({
|
| 416 |
-
prompt: payload.content,
|
| 417 |
-
messageId: payload.id,
|
| 418 |
-
isRetry: true,
|
| 419 |
-
})
|
| 420 |
-
)
|
| 421 |
-
.finally(() => ($loading = false));
|
| 422 |
-
}
|
| 423 |
}
|
| 424 |
|
| 425 |
async function onShowAlternateMsg(payload: { id: Message["id"] }) {
|
|
|
|
| 4 |
import { isAborted } from "$lib/stores/isAborted";
|
| 5 |
import { onMount } from "svelte";
|
| 6 |
import { page } from "$app/state";
|
| 7 |
+
import { beforeNavigate, invalidateAll } from "$app/navigation";
|
| 8 |
import { base } from "$app/paths";
|
| 9 |
import { ERROR_MESSAGES, error } from "$lib/stores/errors";
|
| 10 |
import { findCurrentModel } from "$lib/utils/models";
|
|
|
|
| 95 |
return alternatives;
|
| 96 |
}
|
| 97 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
// this function is used to send new message to the backends
|
| 99 |
async function writeMessage({
|
| 100 |
prompt,
|
|
|
|
| 352 |
});
|
| 353 |
|
| 354 |
async function onMessage(content: string) {
|
| 355 |
+
await writeMessage({ prompt: content });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 356 |
}
|
| 357 |
|
| 358 |
async function onRetry(payload: { id: Message["id"]; content?: string }) {
|
|
|
|
| 361 |
const lastMsgId = payload.id;
|
| 362 |
messagesPath = createMessagesPath(messages, lastMsgId);
|
| 363 |
|
| 364 |
+
await writeMessage({
|
| 365 |
+
prompt: payload.content,
|
| 366 |
+
messageId: payload.id,
|
| 367 |
+
isRetry: true,
|
| 368 |
+
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 369 |
}
|
| 370 |
|
| 371 |
async function onShowAlternateMsg(payload: { id: Message["id"] }) {
|
src/routes/conversation/[id]/+page.ts
CHANGED
|
@@ -8,7 +8,10 @@ export const load = async ({ params, depends, fetch, url }) => {
|
|
| 8 |
const client = useAPIClient({ fetch, origin: url.origin });
|
| 9 |
|
| 10 |
try {
|
| 11 |
-
return await client
|
|
|
|
|
|
|
|
|
|
| 12 |
} catch {
|
| 13 |
redirect(302, "/");
|
| 14 |
}
|
|
|
|
| 8 |
const client = useAPIClient({ fetch, origin: url.origin });
|
| 9 |
|
| 10 |
try {
|
| 11 |
+
return await client
|
| 12 |
+
.conversations({ id: params.id })
|
| 13 |
+
.get({ query: { fromShare: url.searchParams.get("fromShare") ?? undefined } })
|
| 14 |
+
.then(handleResponse);
|
| 15 |
} catch {
|
| 16 |
redirect(302, "/");
|
| 17 |
}
|
src/routes/r/[id]/+page.server.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { redirect } from "@sveltejs/kit";
|
| 2 |
+
import type { PageServerLoad } from "./$types";
|
| 3 |
+
import { loginEnabled } from "$lib/server/auth";
|
| 4 |
+
import { createConversationFromShare } from "$lib/server/conversation";
|
| 5 |
+
|
| 6 |
+
export const load: PageServerLoad = async ({ url, params, locals, request }) => {
|
| 7 |
+
const leafId = url.searchParams.get("leafId");
|
| 8 |
+
|
| 9 |
+
/// if logged in and valid share ID, create conversation from share
|
| 10 |
+
if (loginEnabled && locals.user && params.id) {
|
| 11 |
+
const conversationId = await createConversationFromShare(
|
| 12 |
+
params.id,
|
| 13 |
+
locals,
|
| 14 |
+
request.headers.get("User-Agent") ?? undefined
|
| 15 |
+
);
|
| 16 |
+
|
| 17 |
+
return redirect(
|
| 18 |
+
302,
|
| 19 |
+
`../conversation/${conversationId}?leafId=${leafId}&fromShare=${params.id}`
|
| 20 |
+
);
|
| 21 |
+
}
|
| 22 |
+
return redirect(302, `../conversation/${params.id}?leafId=${leafId}`);
|
| 23 |
+
};
|
src/routes/r/[id]/+page.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
| 1 |
-
import { redirect, type LoadEvent } from "@sveltejs/kit";
|
| 2 |
-
|
| 3 |
-
export const load = async ({ params, url }: LoadEvent) => {
|
| 4 |
-
const leafId = url.searchParams.get("leafId");
|
| 5 |
-
|
| 6 |
-
redirect(302, "../conversation/" + params.id + `?leafId=${leafId}`);
|
| 7 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|