mishig HF Staff nsarrazin HF Staff commited on
Commit
6216bfd
·
unverified ·
1 Parent(s): 662d5af

Simplify frontend for conversation tree representation (#1678)

Browse files

* Simplify frontend for conversation tree representation

* fix lint

* get initialState

* rm unused Message[currenrtChildIdx]

* fix delete branch

* comment

* change lines order

* use existing `assistant` prop in `ChatWindow.svelte`

* Update src/routes/conversation/[id]/+page.svelte

Co-authored-by: Nathan Sarrazin <[email protected]>

* fix: rendering of message when switching branches

---------

Co-authored-by: Nathan Sarrazin <[email protected]>

src/lib/components/chat/Alternatives.svelte CHANGED
@@ -5,11 +5,17 @@
5
  import CarbonChevronRight from "~icons/carbon/chevron-right";
6
 
7
  import { enhance } from "$app/forms";
 
8
 
9
  export let message: Message;
10
- export let childToRender: number;
11
- export let nChildren: number;
12
  export let loading = false;
 
 
 
 
 
 
13
  </script>
14
 
15
  <div
@@ -17,19 +23,21 @@
17
  >
18
  <button
19
  class="inline text-lg font-thin text-gray-400 hover:text-gray-800 disabled:pointer-events-none disabled:opacity-25 dark:text-gray-500 dark:hover:text-gray-200"
20
- on:click={() => (childToRender = Math.max(0, childToRender - 1))}
21
- disabled={childToRender === 0 || loading}
22
  >
23
  <CarbonChevronLeft class="text-sm" />
24
  </button>
25
  <span class=" text-gray-400 dark:text-gray-500">
26
- {childToRender + 1} / {nChildren}
27
  </span>
28
  <button
29
  class="inline text-lg font-thin text-gray-400 hover:text-gray-800 disabled:pointer-events-none disabled:opacity-25 dark:text-gray-500 dark:hover:text-gray-200"
30
  on:click={() =>
31
- (childToRender = Math.min(message?.children?.length ?? 1 - 1, childToRender + 1))}
32
- disabled={childToRender === nChildren - 1 || loading}
 
 
33
  >
34
  <CarbonChevronRight class="text-sm" />
35
  </button>
@@ -43,7 +51,7 @@
43
  }
44
  }}
45
  >
46
- <input name="messageId" value={message.children[childToRender]} type="hidden" />
47
  <button
48
  class="flex items-center justify-center text-xs text-gray-400 hover:text-gray-800 dark:text-gray-500 dark:hover:text-gray-200"
49
  type="submit"
 
5
  import CarbonChevronRight from "~icons/carbon/chevron-right";
6
 
7
  import { enhance } from "$app/forms";
8
+ import { createEventDispatcher } from "svelte";
9
 
10
  export let message: Message;
11
+ export let alternatives: Message["id"][] = [];
 
12
  export let loading = false;
13
+
14
+ $: currentIdx = alternatives.findIndex((id) => id === message.id);
15
+
16
+ const dispatch = createEventDispatcher<{
17
+ showAlternateMsg: { id: Message["id"] };
18
+ }>();
19
  </script>
20
 
21
  <div
 
23
  >
24
  <button
25
  class="inline text-lg font-thin text-gray-400 hover:text-gray-800 disabled:pointer-events-none disabled:opacity-25 dark:text-gray-500 dark:hover:text-gray-200"
26
+ on:click={() => dispatch("showAlternateMsg", { id: alternatives[Math.max(0, currentIdx - 1)] })}
27
+ disabled={currentIdx === 0 || loading}
28
  >
29
  <CarbonChevronLeft class="text-sm" />
30
  </button>
31
  <span class=" text-gray-400 dark:text-gray-500">
32
+ {currentIdx + 1} / {alternatives.length}
33
  </span>
34
  <button
35
  class="inline text-lg font-thin text-gray-400 hover:text-gray-800 disabled:pointer-events-none disabled:opacity-25 dark:text-gray-500 dark:hover:text-gray-200"
36
  on:click={() =>
37
+ dispatch("showAlternateMsg", {
38
+ id: alternatives[Math.min(alternatives.length - 1, currentIdx + 1)],
39
+ })}
40
+ disabled={currentIdx === alternatives.length - 1 || loading}
41
  >
42
  <CarbonChevronRight class="text-sm" />
43
  </button>
 
51
  }
52
  }}
53
  >
54
+ <input name="messageId" value={message.id} type="hidden" />
55
  <button
56
  class="flex items-center justify-center text-xs text-gray-400 hover:text-gray-800 dark:text-gray-500 dark:hover:text-gray-200"
57
  type="submit"
src/lib/components/chat/ChatMessage.svelte CHANGED
@@ -24,25 +24,21 @@
24
  MessageReasoningUpdateType,
25
  } from "$lib/types/MessageUpdate";
26
  import { base } from "$app/paths";
27
- import { useConvTreeStore } from "$lib/stores/convTree";
28
  import ToolUpdate from "./ToolUpdate.svelte";
29
  import { useSettingsStore } from "$lib/stores/settings";
30
- import { browser } from "$app/environment";
31
  import MarkdownRenderer from "./MarkdownRenderer.svelte";
32
  import OpenReasoningResults from "./OpenReasoningResults.svelte";
33
  import Alternatives from "./Alternatives.svelte";
34
  import Vote from "./Vote.svelte";
35
 
36
- export let id: Message["id"];
37
- export let messages: Message[];
38
  export let loading = false;
39
  export let isAuthor = true;
40
  export let readOnly = false;
41
  export let isTapped = false;
42
-
43
- $: message = messages.find((m) => m.id === id) ?? ({} as Message);
44
-
45
- $: urlNotTrailing = $page.url.pathname.replace(/\/$/, "");
46
 
47
  const dispatch = createEventDispatcher<{
48
  retry: { content?: string; id: Message["id"] };
@@ -53,8 +49,6 @@
53
  let pendingTimeout: ReturnType<typeof setTimeout>;
54
  let isCopied = false;
55
 
56
- let initialized = false;
57
-
58
  $: emptyLoad =
59
  !message.content && (webSearchIsDone || (searchUpdates && searchUpdates.length === 0));
60
 
@@ -111,6 +105,7 @@
111
  return acc;
112
  }, {} as Record<string, MessageToolUpdate[]>);
113
 
 
114
  $: downloadLink = urlNotTrailing + `/message/${message.id}/prompt`;
115
 
116
  let webSearchIsDone = true;
@@ -130,7 +125,7 @@
130
  }, 1000);
131
  }
132
 
133
- $: editMode = $convTreeStore.editing === message.id;
134
  let editContentEl: HTMLTextAreaElement;
135
  let editFormEl: HTMLFormElement;
136
 
@@ -141,39 +136,6 @@
141
  editContentEl?.focus();
142
  }
143
  }
144
-
145
- $: isLast = (message && message.children?.length === 0) ?? false;
146
-
147
- $: childToRender = 0;
148
- $: nChildren = message?.children?.length ?? 0;
149
-
150
- $: {
151
- if (initialized) {
152
- childToRender = Math.max(0, nChildren - 1);
153
- } else {
154
- childToRender = 0;
155
- initialized = true;
156
- }
157
- }
158
- const convTreeStore = useConvTreeStore();
159
-
160
- $: if (message.children?.length === 0) {
161
- $convTreeStore.leaf = message.id;
162
- // Check if the code is running in a browser
163
- if (browser) {
164
- // Remember the last message viewed or interacted by the user
165
- localStorage.setItem("leafId", message.id);
166
- }
167
- }
168
-
169
- let isRun = false;
170
- $: {
171
- if (message.id && !isRun) {
172
- if (message.currentChildIndex) childToRender = message.currentChildIndex;
173
- isRun = true;
174
- }
175
- }
176
- $: if (message.children?.length === 0) $convTreeStore.leaf = message.id;
177
  </script>
178
 
179
  {#if message.from === "assistant"}
@@ -323,7 +285,9 @@
323
  </div>
324
  {/if}
325
  </div>
326
- <slot name="childrenNav" />
 
 
327
  {/if}
328
  {#if message.from === "user"}
329
  <div
@@ -356,7 +320,7 @@
356
  bind:this={editFormEl}
357
  on:submit|preventDefault={() => {
358
  dispatch("retry", { content: editContentEl.value, id: message.id });
359
- $convTreeStore.editing = null;
360
  }}
361
  >
362
  <textarea
@@ -383,7 +347,7 @@
383
  type="button"
384
  class="btn rounded-sm p-2 text-sm text-gray-400 hover:text-gray-500 focus:ring-0 dark:text-gray-400 dark:hover:text-gray-300"
385
  on:click={() => {
386
- $convTreeStore.editing = null;
387
  }}
388
  >
389
  Cancel
@@ -415,7 +379,7 @@
415
  class="cursor-pointer rounded-lg border border-gray-100 bg-gray-100 p-1 text-xs text-gray-400 group-hover:block hover:text-gray-500 dark:border-gray-800 dark:bg-gray-800 dark:text-gray-400 dark:hover:text-gray-300 md:hidden lg:-right-2"
416
  title="Branch"
417
  type="button"
418
- on:click={() => ($convTreeStore.editing = message.id)}
419
  >
420
  <CarbonPen />
421
  </button>
@@ -424,33 +388,13 @@
424
  </div>
425
  {/if}
426
  </div>
427
- <slot name="childrenNav" />
 
 
428
  </div>
429
  </div>
430
  {/if}
431
 
432
- {#if nChildren > 0}
433
- {@const messageId = messages.find((m) => m.id === id)?.children?.[childToRender]}
434
- {#key messageId}
435
- <svelte:self
436
- {loading}
437
- {messages}
438
- {isAuthor}
439
- {readOnly}
440
- id={messageId}
441
- on:retry
442
- on:vote
443
- on:continue
444
- >
445
- <svelte:fragment slot="childrenNav">
446
- {#if nChildren > 1 && $convTreeStore.editing === null}
447
- <Alternatives {message} bind:childToRender {nChildren} {loading} />
448
- {/if}
449
- </svelte:fragment>
450
- </svelte:self>
451
- {/key}
452
- {/if}
453
-
454
  <style>
455
  @keyframes loading {
456
  to {
 
24
  MessageReasoningUpdateType,
25
  } from "$lib/types/MessageUpdate";
26
  import { base } from "$app/paths";
 
27
  import ToolUpdate from "./ToolUpdate.svelte";
28
  import { useSettingsStore } from "$lib/stores/settings";
 
29
  import MarkdownRenderer from "./MarkdownRenderer.svelte";
30
  import OpenReasoningResults from "./OpenReasoningResults.svelte";
31
  import Alternatives from "./Alternatives.svelte";
32
  import Vote from "./Vote.svelte";
33
 
34
+ export let message: Message;
 
35
  export let loading = false;
36
  export let isAuthor = true;
37
  export let readOnly = false;
38
  export let isTapped = false;
39
+ export let alternatives: Message["id"][] = [];
40
+ export let editMsdgId: Message["id"] | null = null;
41
+ export let isLast = false;
 
42
 
43
  const dispatch = createEventDispatcher<{
44
  retry: { content?: string; id: Message["id"] };
 
49
  let pendingTimeout: ReturnType<typeof setTimeout>;
50
  let isCopied = false;
51
 
 
 
52
  $: emptyLoad =
53
  !message.content && (webSearchIsDone || (searchUpdates && searchUpdates.length === 0));
54
 
 
105
  return acc;
106
  }, {} as Record<string, MessageToolUpdate[]>);
107
 
108
+ $: urlNotTrailing = $page.url.pathname.replace(/\/$/, "");
109
  $: downloadLink = urlNotTrailing + `/message/${message.id}/prompt`;
110
 
111
  let webSearchIsDone = true;
 
125
  }, 1000);
126
  }
127
 
128
+ $: editMode = editMsdgId === message.id;
129
  let editContentEl: HTMLTextAreaElement;
130
  let editFormEl: HTMLFormElement;
131
 
 
136
  editContentEl?.focus();
137
  }
138
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  </script>
140
 
141
  {#if message.from === "assistant"}
 
285
  </div>
286
  {/if}
287
  </div>
288
+ {#if alternatives.length > 1 && editMsdgId === null}
289
+ <Alternatives {message} {alternatives} {loading} on:showAlternateMsg />
290
+ {/if}
291
  {/if}
292
  {#if message.from === "user"}
293
  <div
 
320
  bind:this={editFormEl}
321
  on:submit|preventDefault={() => {
322
  dispatch("retry", { content: editContentEl.value, id: message.id });
323
+ editMsdgId = null;
324
  }}
325
  >
326
  <textarea
 
347
  type="button"
348
  class="btn rounded-sm p-2 text-sm text-gray-400 hover:text-gray-500 focus:ring-0 dark:text-gray-400 dark:hover:text-gray-300"
349
  on:click={() => {
350
+ editMsdgId = null;
351
  }}
352
  >
353
  Cancel
 
379
  class="cursor-pointer rounded-lg border border-gray-100 bg-gray-100 p-1 text-xs text-gray-400 group-hover:block hover:text-gray-500 dark:border-gray-800 dark:bg-gray-800 dark:text-gray-400 dark:hover:text-gray-300 md:hidden lg:-right-2"
380
  title="Branch"
381
  type="button"
382
+ on:click={() => (editMsdgId = message.id)}
383
  >
384
  <CarbonPen />
385
  </button>
 
388
  </div>
389
  {/if}
390
  </div>
391
+ {#if alternatives.length > 1 && editMsdgId === null}
392
+ <Alternatives {message} {alternatives} {loading} on:showAlternateMsg />
393
+ {/if}
394
  </div>
395
  </div>
396
  {/if}
397
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
398
  <style>
399
  @keyframes loading {
400
  to {
src/lib/components/chat/ChatWindow.svelte CHANGED
@@ -26,7 +26,6 @@
26
  import { snapScrollToBottom } from "$lib/actions/snapScrollToBottom";
27
  import SystemPromptModal from "../SystemPromptModal.svelte";
28
  import ChatIntroduction from "./ChatIntroduction.svelte";
29
- import { useConvTreeStore } from "$lib/stores/convTree";
30
  import UploadedFile from "./UploadedFile.svelte";
31
  import { useSettingsStore } from "$lib/stores/settings";
32
  import ModelSwitch from "./ModelSwitch.svelte";
@@ -37,6 +36,7 @@
37
  import { loginModalOpen } from "$lib/stores/loginModal";
38
 
39
  export let messages: Message[] = [];
 
40
  export let loading = false;
41
  export let pending = false;
42
 
@@ -52,6 +52,7 @@
52
  let message: string;
53
  let timeout: ReturnType<typeof setTimeout>;
54
  let isSharedRecently = false;
 
55
  $: pastedLongContent = false;
56
  $: $page.params.id && (isSharedRecently = false);
57
 
@@ -121,58 +122,7 @@
121
  }
122
  };
123
 
124
- const convTreeStore = useConvTreeStore();
125
-
126
- const updateCurrentIndex = () => {
127
- const url = new URL($page.url);
128
- let leafId = url.searchParams.get("leafId");
129
-
130
- // Ensure the function is only run in the browser.
131
- if (!browser) return;
132
-
133
- if (leafId) {
134
- // Remove the 'leafId' from the URL to clean up after retrieving it.
135
- url.searchParams.delete("leafId");
136
- history.replaceState(null, "", url.toString());
137
- } else {
138
- // Retrieve the 'leafId' from localStorage if it's not in the URL.
139
- leafId = localStorage.getItem("leafId");
140
- }
141
-
142
- // If a 'leafId' exists, find the corresponding message and update indices.
143
- if (leafId) {
144
- let leafMessage = messages.find((m) => m.id == leafId);
145
- if (!leafMessage?.ancestors) return; // Exit if the message has no ancestors.
146
-
147
- let ancestors = leafMessage.ancestors;
148
-
149
- // Loop through all ancestors to update the current child index.
150
- for (let i = 0; i < ancestors.length; i++) {
151
- let curMessage = messages.find((m) => m.id == ancestors[i]);
152
- if (curMessage?.children) {
153
- for (let j = 0; j < curMessage.children.length; j++) {
154
- // Check if the current message's child matches the next ancestor
155
- // or the leaf itself, and update the currentChildIndex accordingly.
156
- if (i + 1 < ancestors.length) {
157
- if (curMessage.children[j] == ancestors[i + 1]) {
158
- curMessage.currentChildIndex = j;
159
- break;
160
- }
161
- } else {
162
- if (curMessage.children[j] == leafId) {
163
- curMessage.currentChildIndex = j;
164
- break;
165
- }
166
- }
167
- }
168
- }
169
- }
170
- }
171
- };
172
-
173
- updateCurrentIndex();
174
-
175
- $: lastMessage = browser && (messages.find((m) => m.id == $convTreeStore.leaf) as Message);
176
  $: lastIsError =
177
  lastMessage &&
178
  !loading &&
@@ -220,8 +170,8 @@
220
 
221
  $: mimeTypesFromActiveTools = $page.data.tools
222
  .filter((tool: ToolFront) => {
223
- if ($page.data?.assistant) {
224
- return $page.data.assistant.tools?.includes(tool._id);
225
  }
226
  if (currentModel.tools) {
227
  return $settings?.tools?.includes(tool._id) ?? tool.isOnByDefault;
@@ -233,7 +183,7 @@
233
  $: activeMimeTypes = Array.from(
234
  new Set([
235
  ...mimeTypesFromActiveTools, // fetch mime types from active tools either from tool settings or active assistant
236
- ...(currentModel.tools && !$page.data.assistant ? ["application/pdf"] : []), // if its a tool model, we can always enable document parser so we always accept pdfs
237
  ...(currentModel.multimodal ? currentModel.multimodalAcceptedMimetypes ?? ["image/*"] : []), // if its a multimodal model, we always accept images
238
  ])
239
  );
@@ -256,15 +206,14 @@
256
  <div
257
  class="mx-auto flex h-full max-w-3xl flex-col gap-6 px-5 pt-6 sm:gap-8 xl:max-w-4xl xl:pt-10"
258
  >
259
- {#if $page.data?.assistant && !!messages.length}
260
  <a
261
  class="mx-auto flex items-center gap-1.5 rounded-full border border-gray-100 bg-gray-50 py-1 pl-1 pr-3 text-sm text-gray-800 hover:bg-gray-100 dark:border-gray-800 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700"
262
- href="{base}/settings/assistants/{$page.data.assistant._id}"
263
  >
264
- {#if $page.data?.assistant.avatar}
265
  <img
266
- src="{base}/settings/assistants/{$page.data?.assistant._id.toString()}/avatar.jpg?hash=${$page
267
- .data.assistant.avatar}"
268
  alt="Avatar"
269
  class="size-5 rounded-full object-cover"
270
  />
@@ -272,11 +221,11 @@
272
  <div
273
  class="flex size-6 items-center justify-center rounded-full bg-gray-300 font-bold uppercase text-gray-500"
274
  >
275
- {$page.data?.assistant.name[0]}
276
  </div>
277
  {/if}
278
 
279
- {$page.data.assistant.name}
280
  </a>
281
  {:else if preprompt && preprompt != currentModel.preprompt}
282
  <SystemPromptModal preprompt={preprompt ?? ""} />
@@ -284,16 +233,21 @@
284
 
285
  {#if messages.length > 0}
286
  <div class="flex h-max flex-col gap-8 pb-52">
287
- <ChatMessage
288
- {loading}
289
- {messages}
290
- id={messages[0].id}
291
- isAuthor={!shared}
292
- readOnly={isReadOnly}
293
- on:retry
294
- on:vote
295
- on:continue
296
- />
 
 
 
 
 
297
  {#if isReadOnly}
298
  <ModelSwitch {models} {currentModel} />
299
  {/if}
@@ -301,15 +255,12 @@
301
  {:else if pending}
302
  <ChatMessage
303
  loading={true}
304
- messages={[
305
- {
306
- id: "0-0-0-0-0",
307
- content: "",
308
- from: "assistant",
309
- children: [],
310
- },
311
- ]}
312
- id={"0-0-0-0-0"}
313
  isAuthor={!shared}
314
  readOnly={isReadOnly}
315
  />
 
26
  import { snapScrollToBottom } from "$lib/actions/snapScrollToBottom";
27
  import SystemPromptModal from "../SystemPromptModal.svelte";
28
  import ChatIntroduction from "./ChatIntroduction.svelte";
 
29
  import UploadedFile from "./UploadedFile.svelte";
30
  import { useSettingsStore } from "$lib/stores/settings";
31
  import ModelSwitch from "./ModelSwitch.svelte";
 
36
  import { loginModalOpen } from "$lib/stores/loginModal";
37
 
38
  export let messages: Message[] = [];
39
+ export let messagesAlternatives: Message["id"][][] = [];
40
  export let loading = false;
41
  export let pending = false;
42
 
 
52
  let message: string;
53
  let timeout: ReturnType<typeof setTimeout>;
54
  let isSharedRecently = false;
55
+ let editMsdgId: Message["id"] | null = null;
56
  $: pastedLongContent = false;
57
  $: $page.params.id && (isSharedRecently = false);
58
 
 
122
  }
123
  };
124
 
125
+ $: lastMessage = browser && (messages.at(-1) as Message);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  $: lastIsError =
127
  lastMessage &&
128
  !loading &&
 
170
 
171
  $: mimeTypesFromActiveTools = $page.data.tools
172
  .filter((tool: ToolFront) => {
173
+ if (assistant) {
174
+ return assistant.tools?.includes(tool._id);
175
  }
176
  if (currentModel.tools) {
177
  return $settings?.tools?.includes(tool._id) ?? tool.isOnByDefault;
 
183
  $: activeMimeTypes = Array.from(
184
  new Set([
185
  ...mimeTypesFromActiveTools, // fetch mime types from active tools either from tool settings or active assistant
186
+ ...(currentModel.tools && !assistant ? ["application/pdf"] : []), // if its a tool model, we can always enable document parser so we always accept pdfs
187
  ...(currentModel.multimodal ? currentModel.multimodalAcceptedMimetypes ?? ["image/*"] : []), // if its a multimodal model, we always accept images
188
  ])
189
  );
 
206
  <div
207
  class="mx-auto flex h-full max-w-3xl flex-col gap-6 px-5 pt-6 sm:gap-8 xl:max-w-4xl xl:pt-10"
208
  >
209
+ {#if assistant && !!messages.length}
210
  <a
211
  class="mx-auto flex items-center gap-1.5 rounded-full border border-gray-100 bg-gray-50 py-1 pl-1 pr-3 text-sm text-gray-800 hover:bg-gray-100 dark:border-gray-800 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700"
212
+ href="{base}/settings/assistants/{assistant._id}"
213
  >
214
+ {#if assistant.avatar}
215
  <img
216
+ src="{base}/settings/assistants/{assistant._id.toString()}/avatar.jpg?hash=${assistant.avatar}"
 
217
  alt="Avatar"
218
  class="size-5 rounded-full object-cover"
219
  />
 
221
  <div
222
  class="flex size-6 items-center justify-center rounded-full bg-gray-300 font-bold uppercase text-gray-500"
223
  >
224
+ {assistant.name[0]}
225
  </div>
226
  {/if}
227
 
228
+ {assistant.name}
229
  </a>
230
  {:else if preprompt && preprompt != currentModel.preprompt}
231
  <SystemPromptModal preprompt={preprompt ?? ""} />
 
233
 
234
  {#if messages.length > 0}
235
  <div class="flex h-max flex-col gap-8 pb-52">
236
+ {#each messages as message, idx (message.id)}
237
+ <ChatMessage
238
+ {loading}
239
+ {message}
240
+ alternatives={messagesAlternatives.find((a) => a.includes(message.id)) ?? []}
241
+ isAuthor={!shared}
242
+ readOnly={isReadOnly}
243
+ isLast={idx === messages.length - 1}
244
+ bind:editMsdgId
245
+ on:retry
246
+ on:vote
247
+ on:continue
248
+ on:showAlternateMsg
249
+ />
250
+ {/each}
251
  {#if isReadOnly}
252
  <ModelSwitch {models} {currentModel} />
253
  {/if}
 
255
  {:else if pending}
256
  <ChatMessage
257
  loading={true}
258
+ message={{
259
+ id: "0-0-0-0-0",
260
+ content: "",
261
+ from: "assistant",
262
+ children: [],
263
+ }}
 
 
 
264
  isAuthor={!shared}
265
  readOnly={isReadOnly}
266
  />
src/lib/stores/convTree.ts DELETED
@@ -1,25 +0,0 @@
1
- import type { Message } from "$lib/types/Message";
2
- import { getContext, setContext } from "svelte";
3
- import { writable, type Writable } from "svelte/store";
4
-
5
- // used to store the id of the message that is the currently displayed leaf of the conversation tree
6
- // (that is the last message in the current branch of the conversation tree)
7
-
8
- interface ConvTreeStore {
9
- leaf: Message["id"] | null;
10
- editing: Message["id"] | null;
11
- }
12
-
13
- export function useConvTreeStore() {
14
- return getContext<Writable<ConvTreeStore>>("convTreeStore");
15
- }
16
-
17
- export function createConvTreeStore() {
18
- const convTreeStore = writable<ConvTreeStore>({
19
- leaf: null,
20
- editing: null,
21
- });
22
- setContext("convTreeStore", convTreeStore);
23
-
24
- return convTreeStore;
25
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/types/Message.ts CHANGED
@@ -25,9 +25,6 @@ export type Message = Partial<Timestamps> & {
25
 
26
  // goes one level deep
27
  children?: Message["id"][];
28
-
29
- // the index of the current child in the children array of the message if the message has more than one child
30
- currentChildIndex?: number;
31
  };
32
 
33
  export type MessageFile = {
 
25
 
26
  // goes one level deep
27
  children?: Message["id"][];
 
 
 
28
  };
29
 
30
  export type MessageFile = {
src/routes/conversation/[id]/+page.svelte CHANGED
@@ -21,9 +21,9 @@
21
  import { addChildren } from "$lib/utils/tree/addChildren";
22
  import { addSibling } from "$lib/utils/tree/addSibling";
23
  import { fetchMessageUpdates } from "$lib/utils/messageUpdates";
24
- import { createConvTreeStore } from "$lib/stores/convTree";
25
  import type { v4 } from "uuid";
26
  import { useSettingsStore } from "$lib/stores/settings.js";
 
27
 
28
  export let data;
29
 
@@ -31,11 +31,73 @@
31
 
32
  let loading = false;
33
  let pending = false;
 
34
 
35
  $: activeModel = findCurrentModel([...data.models, ...data.oldModels], data.model);
36
 
37
  let files: File[] = [];
38
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  async function convFromShared() {
40
  try {
41
  loading = true;
@@ -68,7 +130,7 @@
68
  // this function is used to send new message to the backends
69
  async function writeMessage({
70
  prompt,
71
- messageId = $convTreeStore.leaf ?? undefined,
72
  isRetry = false,
73
  isContinue = false,
74
  }: {
@@ -338,6 +400,9 @@
338
  }
339
 
340
  async function onRetry(event: CustomEvent<{ id: Message["id"]; content?: string }>) {
 
 
 
341
  if (!data.shared) {
342
  await writeMessage({
343
  prompt: event.detail.content,
@@ -361,6 +426,11 @@
361
  }
362
  }
363
 
 
 
 
 
 
364
  async function onContinue(event: CustomEvent<{ id: Message["id"] }>) {
365
  if (!data.shared) {
366
  await writeMessage({ messageId: event.detail.id, isContinue: true });
@@ -380,10 +450,9 @@
380
  }
381
  }
382
 
383
- $: $page.params.id, (($isAborted = true), (loading = false), ($convTreeStore.editing = null));
384
  $: title = data.conversations.find((conv) => conv.id === $page.params.id)?.title ?? data.title;
385
 
386
- const convTreeStore = createConvTreeStore();
387
  const settings = useSettingsStore();
388
  </script>
389
 
@@ -402,13 +471,15 @@
402
  <ChatWindow
403
  {loading}
404
  {pending}
405
- {messages}
 
406
  shared={data.shared}
407
  preprompt={data.preprompt}
408
  bind:files
409
  on:message={onMessage}
410
  on:retry={onRetry}
411
  on:continue={onContinue}
 
412
  on:vote={(event) => voteMessage(event.detail.score, event.detail.id)}
413
  on:share={() => shareConversation($page.params.id, data.title)}
414
  on:stop={() => (($isAborted = true), (loading = false))}
 
21
  import { addChildren } from "$lib/utils/tree/addChildren";
22
  import { addSibling } from "$lib/utils/tree/addSibling";
23
  import { fetchMessageUpdates } from "$lib/utils/messageUpdates";
 
24
  import type { v4 } from "uuid";
25
  import { useSettingsStore } from "$lib/stores/settings.js";
26
+ import { browser } from "$app/environment";
27
 
28
  export let data;
29
 
 
31
 
32
  let loading = false;
33
  let pending = false;
34
+ let initialRun = true;
35
 
36
  $: activeModel = findCurrentModel([...data.models, ...data.oldModels], data.model);
37
 
38
  let files: File[] = [];
39
 
40
+ // create a linear list of `messagesPath` from `messages` that is a tree of threaded messages
41
+ $: messagesPath = createMessagesPath(messages);
42
+ $: messagesAlternatives = createMessagesAlternatives(messages);
43
+
44
+ $: if (browser && messagesPath.at(-1)?.id) {
45
+ localStorage.setItem("leafId", messagesPath.at(-1)?.id as string);
46
+ }
47
+
48
+ function createMessagesPath(messages: Message[], msgId?: Message["id"]): Message[] {
49
+ if (initialRun) {
50
+ if (!msgId && $page.url.searchParams.get("leafId")) {
51
+ msgId = $page.url.searchParams.get("leafId") as string;
52
+ $page.url.searchParams.delete("leafId");
53
+ }
54
+ if (!msgId && browser && localStorage.getItem("leafId")) {
55
+ msgId = localStorage.getItem("leafId") as string;
56
+ }
57
+ initialRun = false;
58
+ }
59
+
60
+ const msg = messages.find((msg) => msg.id === msgId) ?? messages.at(-1);
61
+ if (!msg) return [];
62
+ // ancestor path
63
+ const { ancestors } = msg;
64
+ const path = [];
65
+ if (ancestors?.length) {
66
+ for (const ancestorId of ancestors) {
67
+ const ancestor = messages.find((msg) => msg.id === ancestorId);
68
+ if (ancestor) {
69
+ path.push(ancestor);
70
+ }
71
+ }
72
+ }
73
+
74
+ // push the node itself in the middle
75
+ path.push(msg);
76
+
77
+ // children path
78
+ let childrenIds = msg.children;
79
+ while (childrenIds?.length) {
80
+ let lastChildId = childrenIds.at(-1);
81
+ const lastChild = messages.find((msg) => msg.id === lastChildId);
82
+ if (lastChild) {
83
+ path.push(lastChild);
84
+ }
85
+ childrenIds = lastChild?.children;
86
+ }
87
+
88
+ return path;
89
+ }
90
+
91
+ function createMessagesAlternatives(messages: Message[]): Message["id"][][] {
92
+ const alternatives = [];
93
+ for (const message of messages) {
94
+ if (message.children?.length) {
95
+ alternatives.push(message.children);
96
+ }
97
+ }
98
+ return alternatives;
99
+ }
100
+
101
  async function convFromShared() {
102
  try {
103
  loading = true;
 
130
  // this function is used to send new message to the backends
131
  async function writeMessage({
132
  prompt,
133
+ messageId = messagesPath.at(-1)?.id ?? undefined,
134
  isRetry = false,
135
  isContinue = false,
136
  }: {
 
400
  }
401
 
402
  async function onRetry(event: CustomEvent<{ id: Message["id"]; content?: string }>) {
403
+ const lastMsgId = event.detail.id;
404
+ messagesPath = createMessagesPath(messages, lastMsgId);
405
+
406
  if (!data.shared) {
407
  await writeMessage({
408
  prompt: event.detail.content,
 
426
  }
427
  }
428
 
429
+ async function onShowAlternateMsg(event: CustomEvent<{ id: Message["id"] }>) {
430
+ const msgId = event.detail.id;
431
+ messagesPath = createMessagesPath(messages, msgId);
432
+ }
433
+
434
  async function onContinue(event: CustomEvent<{ id: Message["id"] }>) {
435
  if (!data.shared) {
436
  await writeMessage({ messageId: event.detail.id, isContinue: true });
 
450
  }
451
  }
452
 
453
+ $: $page.params.id, (($isAborted = true), (loading = false));
454
  $: title = data.conversations.find((conv) => conv.id === $page.params.id)?.title ?? data.title;
455
 
 
456
  const settings = useSettingsStore();
457
  </script>
458
 
 
471
  <ChatWindow
472
  {loading}
473
  {pending}
474
+ messages={messagesPath}
475
+ {messagesAlternatives}
476
  shared={data.shared}
477
  preprompt={data.preprompt}
478
  bind:files
479
  on:message={onMessage}
480
  on:retry={onRetry}
481
  on:continue={onContinue}
482
+ on:showAlternateMsg={onShowAlternateMsg}
483
  on:vote={(event) => voteMessage(event.detail.score, event.detail.id)}
484
  on:share={() => shareConversation($page.params.id, data.title)}
485
  on:stop={() => (($isAborted = true), (loading = false))}