Andrew commited on
Commit
f41db77
·
1 Parent(s): 4123958

(fix) Fix ChatMessage handling of streaming think segments

Browse files
src/lib/components/chat/ChatMessage.svelte CHANGED
@@ -12,17 +12,18 @@
12
  import CarbonPen from "~icons/carbon/pen";
13
  import UploadedFile from "./UploadedFile.svelte";
14
 
15
- import {
16
- MessageUpdateType,
17
- type MessageReasoningUpdate,
18
- MessageReasoningUpdateType,
19
- } from "$lib/types/MessageUpdate";
20
  import MarkdownRenderer from "./MarkdownRenderer.svelte";
21
  import OpenReasoningResults from "./OpenReasoningResults.svelte";
22
  import Alternatives from "./Alternatives.svelte";
23
  import MessageAvatar from "./MessageAvatar.svelte";
24
  import PersonaResponseCarousel from "./PersonaResponseCarousel.svelte";
25
- import { THINK_BLOCK_REGEX } from "$lib/constants/thinkBlockRegex";
 
26
 
27
  interface Props {
28
  message: Message;
@@ -92,14 +93,14 @@
92
  // const urlNotTrailing = $derived(page.url.pathname.replace(/\/$/, ""));
93
  // let downloadLink = $derived(urlNotTrailing + `/message/${message.id}/prompt`);
94
 
95
- let thinkSegments = $derived.by(() => message.content.split(THINK_BLOCK_REGEX));
96
  let hasServerReasoning = $derived(
97
  reasoningUpdates &&
98
  reasoningUpdates.length > 0 &&
99
  !!message.reasoning &&
100
  message.reasoning.trim().length > 0
101
  );
102
- let hasClientThink = $derived(!hasServerReasoning && thinkSegments.length > 1);
103
 
104
  $effect(() => {
105
  if (isCopied) {
@@ -193,28 +194,27 @@
193
  <IconLoading classNames="loading inline ml-2 first:ml-0" />
194
  {/if}
195
 
196
- {#if hasClientThink}
197
- {#each message.content.split(THINK_BLOCK_REGEX) as part, _i}
198
- {#if part && part.startsWith("<think>")}
199
- {@const isClosed = part.endsWith("</think>")}
200
- {@const thinkContent = part.slice(7, isClosed ? -8 : undefined)}
201
- {@const summary = isClosed
202
- ? thinkContent.trim().split(/\n+/)[0] || "Reasoning"
203
- : "Thinking..."}
204
 
205
- <OpenReasoningResults
206
- {summary}
207
- content={thinkContent}
208
- loading={isLast && loading && !isClosed}
209
- />
210
- {:else if part && part.trim().length > 0}
211
- <div
212
- class="prose max-w-none dark:prose-invert max-sm:prose-sm prose-headings:font-semibold prose-h1:text-lg prose-h2:text-base prose-h3:text-base prose-pre:bg-gray-800 dark:prose-pre:bg-gray-900"
213
- >
214
- <MarkdownRenderer content={part} loading={isLast && loading} />
215
- </div>
216
  {/if}
217
- {/each}
 
 
 
 
 
 
 
218
  {:else}
219
  <div
220
  class="prose max-w-none dark:prose-invert max-sm:prose-sm prose-headings:font-semibold prose-h1:text-lg prose-h2:text-base prose-h3:text-base prose-pre:bg-gray-800 dark:prose-pre:bg-gray-900"
 
12
  import CarbonPen from "~icons/carbon/pen";
13
  import UploadedFile from "./UploadedFile.svelte";
14
 
15
+ import {
16
+ MessageUpdateType,
17
+ type MessageReasoningUpdate,
18
+ MessageReasoningUpdateType,
19
+ } from "$lib/types/MessageUpdate";
20
  import MarkdownRenderer from "./MarkdownRenderer.svelte";
21
  import OpenReasoningResults from "./OpenReasoningResults.svelte";
22
  import Alternatives from "./Alternatives.svelte";
23
  import MessageAvatar from "./MessageAvatar.svelte";
24
  import PersonaResponseCarousel from "./PersonaResponseCarousel.svelte";
25
+ import ThinkingPlaceholder from "./ThinkingPlaceholder.svelte";
26
+ import { hasThinkSegments, splitThinkSegments } from "$lib/utils/stripThinkBlocks";
27
 
28
  interface Props {
29
  message: Message;
 
93
  // const urlNotTrailing = $derived(page.url.pathname.replace(/\/$/, ""));
94
  // let downloadLink = $derived(urlNotTrailing + `/message/${message.id}/prompt`);
95
 
96
+ let thinkSegments = $derived.by(() => splitThinkSegments(message.content));
97
  let hasServerReasoning = $derived(
98
  reasoningUpdates &&
99
  reasoningUpdates.length > 0 &&
100
  !!message.reasoning &&
101
  message.reasoning.trim().length > 0
102
  );
103
+ let hasClientThink = $derived(!hasServerReasoning && hasThinkSegments(message.content));
104
 
105
  $effect(() => {
106
  if (isCopied) {
 
194
  <IconLoading classNames="loading inline ml-2 first:ml-0" />
195
  {/if}
196
 
197
+ {#if hasClientThink}
198
+ {#each thinkSegments as part, _i}
199
+ {#if part && part.startsWith("<think>")}
200
+ {@const trimmed = part.trimEnd()}
201
+ {@const isClosed = trimmed.endsWith("</think>")}
 
 
 
202
 
203
+ {#if isClosed}
204
+ {@const thinkContent = trimmed.slice(7, -8)}
205
+ {@const summary = thinkContent.trim().split(/\n+/)[0] || "Reasoning"}
206
+ <OpenReasoningResults {summary} content={thinkContent} loading={false} />
207
+ {:else}
208
+ <ThinkingPlaceholder />
 
 
 
 
 
209
  {/if}
210
+ {:else if part && part.trim().length > 0}
211
+ <div
212
+ class="prose max-w-none dark:prose-invert max-sm:prose-sm prose-headings:font-semibold prose-h1:text-lg prose-h2:text-base prose-h3:text-base prose-pre:bg-gray-800 dark:prose-pre:bg-gray-900"
213
+ >
214
+ <MarkdownRenderer content={part} loading={isLast && loading} />
215
+ </div>
216
+ {/if}
217
+ {/each}
218
  {:else}
219
  <div
220
  class="prose max-w-none dark:prose-invert max-sm:prose-sm prose-headings:font-semibold prose-h1:text-lg prose-h2:text-base prose-h3:text-base prose-pre:bg-gray-800 dark:prose-pre:bg-gray-900"