Spaces:
Sleeping
Maintain Conversation Tree State on Refresh or Share (#1397)
Browse files* Saving and reloading the current tree on page refresh
* Saving and reloading the current tree on page refresh / Moving to the last branch when editing a user message or retrying an assistant message.
* Update Reload the current tree state on page refresh
* updated updateCurrentIndex function
* Update updateCurrentIndex Function
* Persist current tree in navigation/shared conversations v1
Co-authored-by: Kenneth Ramos <kenneth.ramos95@gmail.com>
Co-authored-by: Eduardo Rocha <rocha092@csusm.edu>
* Removed uncessary comment.
Co-authored-by: Long Dao <hailongdao1017@gmail.com>
Co-authored-by: Eduardo Rocha <rocha092@csusm.edu>
* Added enhance import and document focus fix.
Co-authored-by: Long Dao <hailongdao1017@gmail.com>
Co-authored-by: Eduardo Rocha <rocha092@csum.edu>
* Fixed code to have updated formatting and LoadEvent type in page.ts
Co-authored-by: Long Dao <hailongdao1017@gmail.com>
Co-authored-by: Eduardo Rocha <rocha092@csusm.edu>
* run formatting
* simplify pr
---------
Co-authored-by: Long Dao <hailongdao1017@gmail.com>
Co-authored-by: Eduardo Rocha <rocha092@csusm.edu>
Co-authored-by: Eduardo Rocha <rocha092@csum.edu>
Co-authored-by: Nathan Sarrazin <sarrazin.nathan@gmail.com>
|
@@ -34,6 +34,7 @@
|
|
| 34 |
import { useSettingsStore } from "$lib/stores/settings";
|
| 35 |
import DOMPurify from "isomorphic-dompurify";
|
| 36 |
import { enhance } from "$app/forms";
|
|
|
|
| 37 |
|
| 38 |
function sanitizeMd(md: string) {
|
| 39 |
let ret = md
|
|
@@ -207,6 +208,22 @@
|
|
| 207 |
}
|
| 208 |
const convTreeStore = useConvTreeStore();
|
| 209 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
$: if (message.children?.length === 0) $convTreeStore.leaf = message.id;
|
| 211 |
</script>
|
| 212 |
|
|
@@ -300,9 +317,9 @@
|
|
| 300 |
{#if !loading && (message.content || toolUpdates)}
|
| 301 |
<div
|
| 302 |
class="absolute -bottom-4 right-0 flex max-md:transition-all md:group-hover:visible md:group-hover:opacity-100
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
>
|
| 307 |
{#if isAuthor}
|
| 308 |
<button
|
|
@@ -334,7 +351,9 @@
|
|
| 334 |
class="btn rounded-sm p-1 text-sm text-gray-400 hover:text-gray-500 focus:ring-0 dark:text-gray-400 dark:hover:text-gray-300"
|
| 335 |
title="Retry"
|
| 336 |
type="button"
|
| 337 |
-
on:click={() =>
|
|
|
|
|
|
|
| 338 |
>
|
| 339 |
<CarbonRotate360 />
|
| 340 |
</button>
|
|
@@ -394,7 +413,7 @@
|
|
| 394 |
<button
|
| 395 |
type="submit"
|
| 396 |
class="btn rounded-lg px-3 py-1.5 text-sm
|
| 397 |
-
|
| 398 |
? 'bg-gray-300 text-gray-400 dark:bg-gray-700 dark:text-gray-600'
|
| 399 |
: 'bg-gray-200 text-gray-600 hover:text-gray-800 focus:ring-0 dark:bg-gray-800 dark:text-gray-300 dark:hover:text-gray-200'}
|
| 400 |
"
|
|
@@ -417,8 +436,8 @@
|
|
| 417 |
{#if !loading && !editMode}
|
| 418 |
<div
|
| 419 |
class="
|
| 420 |
-
|
| 421 |
-
|
| 422 |
isCopied
|
| 423 |
? 'max-md:visible max-md:translate-y-0 max-md:opacity-100'
|
| 424 |
: ''}"
|
|
|
|
| 34 |
import { useSettingsStore } from "$lib/stores/settings";
|
| 35 |
import DOMPurify from "isomorphic-dompurify";
|
| 36 |
import { enhance } from "$app/forms";
|
| 37 |
+
import { browser } from "$app/environment";
|
| 38 |
|
| 39 |
function sanitizeMd(md: string) {
|
| 40 |
let ret = md
|
|
|
|
| 208 |
}
|
| 209 |
const convTreeStore = useConvTreeStore();
|
| 210 |
|
| 211 |
+
$: if (message.children?.length === 0) {
|
| 212 |
+
$convTreeStore.leaf = message.id;
|
| 213 |
+
// Check if the code is running in a browser
|
| 214 |
+
if (browser) {
|
| 215 |
+
// Remember the last message viewed or interacted by the user
|
| 216 |
+
localStorage.setItem("leafId", message.id);
|
| 217 |
+
}
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
let isRun = false;
|
| 221 |
+
$: {
|
| 222 |
+
if (message.id && !isRun) {
|
| 223 |
+
if (message.currentChildIndex) childrenToRender = message.currentChildIndex;
|
| 224 |
+
isRun = true;
|
| 225 |
+
}
|
| 226 |
+
}
|
| 227 |
$: if (message.children?.length === 0) $convTreeStore.leaf = message.id;
|
| 228 |
</script>
|
| 229 |
|
|
|
|
| 317 |
{#if !loading && (message.content || toolUpdates)}
|
| 318 |
<div
|
| 319 |
class="absolute -bottom-4 right-0 flex max-md:transition-all md:group-hover:visible md:group-hover:opacity-100
|
| 320 |
+
{message.score ? 'visible opacity-100' : 'invisible max-md:-translate-y-4 max-md:opacity-0'}
|
| 321 |
+
{isTapped || isCopied ? 'max-md:visible max-md:translate-y-0 max-md:opacity-100' : ''}
|
| 322 |
+
"
|
| 323 |
>
|
| 324 |
{#if isAuthor}
|
| 325 |
<button
|
|
|
|
| 351 |
class="btn rounded-sm p-1 text-sm text-gray-400 hover:text-gray-500 focus:ring-0 dark:text-gray-400 dark:hover:text-gray-300"
|
| 352 |
title="Retry"
|
| 353 |
type="button"
|
| 354 |
+
on:click={() => {
|
| 355 |
+
dispatch("retry", { id: message.id });
|
| 356 |
+
}}
|
| 357 |
>
|
| 358 |
<CarbonRotate360 />
|
| 359 |
</button>
|
|
|
|
| 413 |
<button
|
| 414 |
type="submit"
|
| 415 |
class="btn rounded-lg px-3 py-1.5 text-sm
|
| 416 |
+
{loading
|
| 417 |
? 'bg-gray-300 text-gray-400 dark:bg-gray-700 dark:text-gray-600'
|
| 418 |
: 'bg-gray-200 text-gray-600 hover:text-gray-800 focus:ring-0 dark:bg-gray-800 dark:text-gray-300 dark:hover:text-gray-200'}
|
| 419 |
"
|
|
|
|
| 436 |
{#if !loading && !editMode}
|
| 437 |
<div
|
| 438 |
class="
|
| 439 |
+
max-md:opacity-0' invisible absolute
|
| 440 |
+
right-0 top-3.5 z-10 h-max max-md:-translate-y-4 max-md:transition-all md:bottom-0 md:group-hover:visible md:group-hover:opacity-100 {isTapped ||
|
| 441 |
isCopied
|
| 442 |
? 'max-md:visible max-md:translate-y-0 max-md:opacity-100'
|
| 443 |
: ''}"
|
|
@@ -108,6 +108,55 @@
|
|
| 108 |
|
| 109 |
const convTreeStore = useConvTreeStore();
|
| 110 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
$: lastMessage = browser && (messages.find((m) => m.id == $convTreeStore.leaf) as Message);
|
| 112 |
$: lastIsError =
|
| 113 |
lastMessage &&
|
|
@@ -344,7 +393,7 @@
|
|
| 344 |
aria-label={isFileUploadEnabled ? "file dropzone" : undefined}
|
| 345 |
on:submit|preventDefault={handleSubmit}
|
| 346 |
class="relative flex w-full max-w-4xl flex-1 items-center rounded-xl border bg-gray-100 focus-within:border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:focus-within:border-gray-500
|
| 347 |
-
|
| 348 |
>
|
| 349 |
{#if onDrag && isFileUploadEnabled}
|
| 350 |
<FileDropzone bind:files bind:onDrag mimeTypes={activeMimeTypes} />
|
|
|
|
| 108 |
|
| 109 |
const convTreeStore = useConvTreeStore();
|
| 110 |
|
| 111 |
+
const updateCurrentIndex = () => {
|
| 112 |
+
const url = new URL($page.url);
|
| 113 |
+
let leafId = url.searchParams.get("leafId");
|
| 114 |
+
|
| 115 |
+
// Ensure the function is only run in the browser.
|
| 116 |
+
if (!browser) return;
|
| 117 |
+
|
| 118 |
+
if (leafId) {
|
| 119 |
+
// Remove the 'leafId' from the URL to clean up after retrieving it.
|
| 120 |
+
url.searchParams.delete("leafId");
|
| 121 |
+
history.replaceState(null, "", url.toString());
|
| 122 |
+
} else {
|
| 123 |
+
// Retrieve the 'leafId' from localStorage if it's not in the URL.
|
| 124 |
+
leafId = localStorage.getItem("leafId");
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
// If a 'leafId' exists, find the corresponding message and update indices.
|
| 128 |
+
if (leafId) {
|
| 129 |
+
let leafMessage = messages.find((m) => m.id == leafId);
|
| 130 |
+
if (!leafMessage?.ancestors) return; // Exit if the message has no ancestors.
|
| 131 |
+
|
| 132 |
+
let ancestors = leafMessage.ancestors;
|
| 133 |
+
|
| 134 |
+
// Loop through all ancestors to update the current child index.
|
| 135 |
+
for (let i = 0; i < ancestors.length; i++) {
|
| 136 |
+
let curMessage = messages.find((m) => m.id == ancestors[i]);
|
| 137 |
+
if (curMessage?.children) {
|
| 138 |
+
for (let j = 0; j < curMessage.children.length; j++) {
|
| 139 |
+
// Check if the current message's child matches the next ancestor
|
| 140 |
+
// or the leaf itself, and update the currentChildIndex accordingly.
|
| 141 |
+
if (i + 1 < ancestors.length) {
|
| 142 |
+
if (curMessage.children[j] == ancestors[i + 1]) {
|
| 143 |
+
curMessage.currentChildIndex = j;
|
| 144 |
+
break;
|
| 145 |
+
}
|
| 146 |
+
} else {
|
| 147 |
+
if (curMessage.children[j] == leafId) {
|
| 148 |
+
curMessage.currentChildIndex = j;
|
| 149 |
+
break;
|
| 150 |
+
}
|
| 151 |
+
}
|
| 152 |
+
}
|
| 153 |
+
}
|
| 154 |
+
}
|
| 155 |
+
}
|
| 156 |
+
};
|
| 157 |
+
|
| 158 |
+
updateCurrentIndex();
|
| 159 |
+
|
| 160 |
$: lastMessage = browser && (messages.find((m) => m.id == $convTreeStore.leaf) as Message);
|
| 161 |
$: lastIsError =
|
| 162 |
lastMessage &&
|
|
|
|
| 393 |
aria-label={isFileUploadEnabled ? "file dropzone" : undefined}
|
| 394 |
on:submit|preventDefault={handleSubmit}
|
| 395 |
class="relative flex w-full max-w-4xl flex-1 items-center rounded-xl border bg-gray-100 focus-within:border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:focus-within:border-gray-500
|
| 396 |
+
{isReadOnly ? 'opacity-30' : ''}"
|
| 397 |
>
|
| 398 |
{#if onDrag && isFileUploadEnabled}
|
| 399 |
<FileDropzone bind:files bind:onDrag mimeTypes={activeMimeTypes} />
|
|
@@ -23,6 +23,9 @@ export type Message = Partial<Timestamps> & {
|
|
| 23 |
|
| 24 |
// goes one level deep
|
| 25 |
children?: Message["id"][];
|
|
|
|
|
|
|
|
|
|
| 26 |
};
|
| 27 |
|
| 28 |
export type MessageFile = {
|
|
|
|
| 23 |
|
| 24 |
// goes one level deep
|
| 25 |
children?: Message["id"][];
|
| 26 |
+
|
| 27 |
+
// the index of the current child in the children array of the message if the message has more than one child
|
| 28 |
+
currentChildIndex?: number;
|
| 29 |
};
|
| 30 |
|
| 31 |
export type MessageFile = {
|
|
@@ -1,7 +1,28 @@
|
|
|
|
|
|
|
|
| 1 |
export async function share(url: string, title: string) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
if (navigator.share) {
|
| 3 |
navigator.share({ url, title });
|
| 4 |
} else {
|
| 5 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
}
|
| 7 |
}
|
|
|
|
| 1 |
+
import { browser } from "$app/environment";
|
| 2 |
+
|
| 3 |
export async function share(url: string, title: string) {
|
| 4 |
+
if (!browser) return;
|
| 5 |
+
// Retrieve the leafId from localStorage
|
| 6 |
+
const leafId = localStorage.getItem("leafId");
|
| 7 |
+
if (leafId) {
|
| 8 |
+
// Use URL and URLSearchParams to add the leafId parameter
|
| 9 |
+
const shareUrl = new URL(url);
|
| 10 |
+
shareUrl.searchParams.append("leafId", leafId);
|
| 11 |
+
url = shareUrl.toString();
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
if (navigator.share) {
|
| 15 |
navigator.share({ url, title });
|
| 16 |
} else {
|
| 17 |
+
alert("Please focus the document within 3 seconds by clicking somewhere or pressing Tab.");
|
| 18 |
+
// Document Focus Error Handling
|
| 19 |
+
setTimeout(async () => {
|
| 20 |
+
if (document.hasFocus()) {
|
| 21 |
+
await navigator.clipboard.writeText(url);
|
| 22 |
+
} else {
|
| 23 |
+
console.error("Document is not focused. Unable to write to clipboard.");
|
| 24 |
+
alert("Document is not focused. Please try again.");
|
| 25 |
+
}
|
| 26 |
+
}, 3000); // 3-second delay to allow focusing
|
| 27 |
}
|
| 28 |
}
|
|
@@ -1,5 +1,7 @@
|
|
| 1 |
-
import { redirect } from "@sveltejs/kit";
|
| 2 |
|
| 3 |
-
export const load = async ({ params }) => {
|
| 4 |
-
|
|
|
|
|
|
|
| 5 |
};
|
|
|
|
| 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 |
+
throw redirect(302, "../conversation/" + params.id + `?leafId=${leafId}`);
|
| 7 |
};
|