Andrew commited on
Commit
7f69e81
·
1 Parent(s): 6c43717

Remove reasoning results card

Browse files
src/lib/components/chat/ChatMessage.svelte CHANGED
@@ -7,23 +7,22 @@
7
  import CopyToClipBoardBtn from "../CopyToClipBoardBtn.svelte";
8
  import IconLoading from "../icons/IconLoading.svelte";
9
  import CarbonRotate360 from "~icons/carbon/rotate-360";
10
- // import CarbonDownload from "~icons/carbon/download";
11
 
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;
@@ -39,6 +38,9 @@ import { hasThinkSegments, splitThinkSegments } from "$lib/utils/stripThinkBlock
39
  personaStance?: string;
40
  onretry?: (payload: { id: Message["id"]; content?: string; personaId?: string }) => void;
41
  onshowAlternateMsg?: (payload: { id: Message["id"] }) => void;
 
 
 
42
  }
43
 
44
  let {
@@ -55,12 +57,16 @@ import { hasThinkSegments, splitThinkSegments } from "$lib/utils/stripThinkBlock
55
  personaStance,
56
  onretry,
57
  onshowAlternateMsg,
 
 
 
58
  }: Props = $props();
59
 
60
  let contentEl: HTMLElement | undefined = $state();
61
  let isCopied = $state(false);
62
  let messageWidth: number = $state(0);
63
  let messageInfoWidth: number = $state(0);
 
64
 
65
  $effect(() => {
66
  // referenced to appease linter for currently-unused props
@@ -121,6 +127,49 @@ let hasPersonaResponses = $derived((message.personaResponses?.length ?? 0) > 0);
121
  }
122
  }
123
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  </script>
125
 
126
  {#if message.from === "assistant"}
@@ -150,6 +199,10 @@ let hasPersonaResponses = $derived((message.personaResponses?.length ?? 0) > 0);
150
  personaResponses={message.personaResponses}
151
  loading={isLast && loading}
152
  onretry={(personaId: string) => onretry?.({ id: message.id, content: undefined, personaId })}
 
 
 
 
153
  />
154
  </div>
155
  {:else}
@@ -180,67 +233,76 @@ let hasPersonaResponses = $derived((message.personaResponses?.length ?? 0) > 0);
180
  </div>
181
  {/if}
182
 
183
- {#if hasServerReasoning}
184
- {@const summaries = reasoningUpdates
185
- .filter((u) => u.subtype === MessageReasoningUpdateType.Status)
186
- .map((u) => u.status)}
187
 
188
- <OpenReasoningResults
189
- summary={summaries[summaries.length - 1] || ""}
190
- content={message.reasoning || ""}
191
- loading={loading && message.content.length === 0}
192
- />
193
  {/if}
194
 
195
- <div bind:this={contentEl}>
196
- {#if isLast && loading && message.content.length === 0}
197
- <IconLoading classNames="loading inline ml-2 first:ml-0" />
198
- {/if}
199
-
200
  {#if hasClientThink}
201
  {#each thinkSegments as part, _i}
202
  {#if part && part.startsWith("<think>")}
203
  {@const trimmed = part.trimEnd()}
204
  {@const isClosed = trimmed.endsWith("</think>")}
205
 
206
- {#if isClosed}
207
- {@const thinkContent = trimmed.slice(7, -8)}
208
- {@const summary = thinkContent.trim().split(/\n+/)[0] || "Reasoning"}
209
- <OpenReasoningResults {summary} content={thinkContent} loading={false} />
210
- {:else}
211
- <ThinkingPlaceholder />
212
- {/if}
213
- {:else if part && part.trim().length > 0}
214
- <div
215
- 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"
216
- >
217
- <MarkdownRenderer content={part} loading={isLast && loading} />
218
- </div>
219
  {/if}
220
- {/each}
221
- {:else}
222
  <div
223
  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"
224
  >
225
- <MarkdownRenderer content={message.content} loading={isLast && loading} />
226
  </div>
227
  {/if}
228
- </div>
229
-
230
- <!-- Copy button inside the bubble -->
231
- {#if !isLast || !loading}
232
- <div class="mt-2 flex justify-end">
233
- <CopyToClipBoardBtn
234
- onClick={() => {
235
- isCopied = true;
236
- }}
237
- classNames="btn rounded-md p-2 text-sm text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700/50"
238
- value={message.content}
239
- iconClassNames="text-xs"
240
- />
241
  </div>
242
  {/if}
243
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
  {/if}
245
 
246
  {#if message.routerMetadata && (!isLast || !loading)}
 
7
  import CopyToClipBoardBtn from "../CopyToClipBoardBtn.svelte";
8
  import IconLoading from "../icons/IconLoading.svelte";
9
  import CarbonRotate360 from "~icons/carbon/rotate-360";
10
+ import CarbonBranch from "~icons/carbon/branch";
11
 
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 Alternatives from "./Alternatives.svelte";
22
  import MessageAvatar from "./MessageAvatar.svelte";
23
  import PersonaResponseCarousel from "./PersonaResponseCarousel.svelte";
24
+ import ThinkingPlaceholder from "./ThinkingPlaceholder.svelte";
25
+ import { hasThinkSegments, splitThinkSegments } from "$lib/utils/stripThinkBlocks";
26
 
27
  interface Props {
28
  message: Message;
 
38
  personaStance?: string;
39
  onretry?: (payload: { id: Message["id"]; content?: string; personaId?: string }) => void;
40
  onshowAlternateMsg?: (payload: { id: Message["id"] }) => void;
41
+ onbranch?: (messageId: string, personaId: string) => void;
42
+ messageBranches?: any[]; // Branches originating from this message
43
+ onopenbranchmodal?: (messageId: string, personaId: string, branches: any[]) => void;
44
  }
45
 
46
  let {
 
57
  personaStance,
58
  onretry,
59
  onshowAlternateMsg,
60
+ onbranch,
61
+ messageBranches = [],
62
+ onopenbranchmodal,
63
  }: Props = $props();
64
 
65
  let contentEl: HTMLElement | undefined = $state();
66
  let isCopied = $state(false);
67
  let messageWidth: number = $state(0);
68
  let messageInfoWidth: number = $state(0);
69
+ let isBranching = $state(false);
70
 
71
  $effect(() => {
72
  // referenced to appease linter for currently-unused props
 
127
  }
128
  }
129
  });
130
+
131
+ // Get persona ID for single persona mode
132
+ function getPersonaId(): string | undefined {
133
+ // Try to get from message personaResponses first
134
+ if (message.personaResponses && message.personaResponses.length > 0) {
135
+ return message.personaResponses[0].personaId;
136
+ }
137
+ // Fallback: this won't work perfectly but it's a reasonable attempt
138
+ return undefined;
139
+ }
140
+
141
+ // Get branches for this message's persona
142
+ let personaBranches = $derived.by(() => {
143
+ const personaId = getPersonaId();
144
+ if (!personaId) return messageBranches;
145
+ return messageBranches.filter(b => b.branchedFromPersonaId === personaId);
146
+ });
147
+
148
+ // Handle branch creation with animation
149
+ async function handleBranchClick() {
150
+ const personaId = getPersonaId();
151
+ if (!personaId || !onbranch) return;
152
+
153
+ isBranching = true;
154
+ await onbranch(message.id, personaId);
155
+
156
+ setTimeout(() => {
157
+ isBranching = false;
158
+ }, 600);
159
+ }
160
+
161
+ // Handle branch button click
162
+ function handleBranchButtonClick() {
163
+ const personaId = getPersonaId();
164
+ if (!personaId) return;
165
+
166
+ const hasExistingBranches = personaBranches.length > 0;
167
+ if (hasExistingBranches) {
168
+ onopenbranchmodal?.(message.id, personaId, personaBranches);
169
+ } else {
170
+ handleBranchClick();
171
+ }
172
+ }
173
  </script>
174
 
175
  {#if message.from === "assistant"}
 
199
  personaResponses={message.personaResponses}
200
  loading={isLast && loading}
201
  onretry={(personaId: string) => onretry?.({ id: message.id, content: undefined, personaId })}
202
+ messageId={message.id}
203
+ onbranch={onbranch}
204
+ messageBranches={messageBranches}
205
+ onopenbranchmodal={onopenbranchmodal}
206
  />
207
  </div>
208
  {:else}
 
233
  </div>
234
  {/if}
235
 
236
+ {#if hasServerReasoning && loading && message.content.length === 0}
237
+ <!-- Show loading indicator while reasoning is in progress -->
238
+ <ThinkingPlaceholder />
239
+ {/if}
240
 
241
+ <div bind:this={contentEl}>
242
+ {#if isLast && loading && message.content.length === 0 && !hasServerReasoning}
243
+ <IconLoading classNames="loading inline ml-2 first:ml-0" />
 
 
244
  {/if}
245
 
 
 
 
 
 
246
  {#if hasClientThink}
247
  {#each thinkSegments as part, _i}
248
  {#if part && part.startsWith("<think>")}
249
  {@const trimmed = part.trimEnd()}
250
  {@const isClosed = trimmed.endsWith("</think>")}
251
 
252
+ {#if isClosed}
253
+ <!-- Skip closed think tags - don't show reasoning content -->
254
+ {:else}
255
+ <ThinkingPlaceholder />
 
 
 
 
 
 
 
 
 
256
  {/if}
257
+ {:else if part && part.trim().length > 0}
 
258
  <div
259
  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"
260
  >
261
+ <MarkdownRenderer content={part} loading={isLast && loading} />
262
  </div>
263
  {/if}
264
+ {/each}
265
+ {:else}
266
+ <div
267
+ 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"
268
+ >
269
+ <MarkdownRenderer content={message.content} loading={isLast && loading} />
 
 
 
 
 
 
 
270
  </div>
271
  {/if}
272
  </div>
273
+
274
+ </div>
275
+
276
+ <!-- Action bar outside the message border -->
277
+ {#if !isLast || !loading}
278
+ <div class="mt-1.5 flex items-center justify-end gap-1 px-2">
279
+ {#if onbranch && personaName}
280
+ {@const branchCount = personaBranches.length}
281
+ {@const hasExistingBranches = branchCount > 0}
282
+
283
+ <button
284
+ type="button"
285
+ class="flex items-center gap-1 rounded-md px-2 py-1.5 text-sm text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700/50 {isBranching ? 'animate-pulse' : ''}"
286
+ onclick={handleBranchButtonClick}
287
+ aria-label={hasExistingBranches ? "Branch options" : "Branch from this response"}
288
+ title={hasExistingBranches ? "View or create branch" : "Branch from this response"}
289
+ >
290
+ <CarbonBranch class="text-xs" />
291
+ {#if hasExistingBranches}
292
+ <span>({branchCount})</span>
293
+ {/if}
294
+ </button>
295
+ {/if}
296
+ <CopyToClipBoardBtn
297
+ onClick={() => {
298
+ isCopied = true;
299
+ }}
300
+ classNames="btn rounded-md p-2 text-sm text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700/50"
301
+ value={message.content}
302
+ iconClassNames="text-xs"
303
+ />
304
+ </div>
305
+ {/if}
306
  {/if}
307
 
308
  {#if message.routerMetadata && (!isLast || !loading)}
src/lib/components/chat/PersonaResponseCards.svelte CHANGED
@@ -1,7 +1,6 @@
1
  <script lang="ts">
2
  import type { PersonaResponse } from "$lib/types/Message";
3
  import MarkdownRenderer from "./MarkdownRenderer.svelte";
4
- import OpenReasoningResults from "./OpenReasoningResults.svelte";
5
  import CopyToClipBoardBtn from "../CopyToClipBoardBtn.svelte";
6
  import CarbonRotate360 from "~icons/carbon/rotate-360";
7
  import CarbonChevronDown from "~icons/carbon/chevron-down";
@@ -92,35 +91,33 @@ import { hasThinkSegments, splitThinkSegments } from "$lib/utils/stripThinkBlock
92
  class="mt-2"
93
  style={isExpanded ? '' : `max-height: ${MAX_COLLAPSED_HEIGHT}px; overflow: hidden;`}
94
  >
95
- {#if hasClientThink(response.content)}
96
- {@const segments = splitThinkSegments(response.content ?? "")}
97
- {#each segments as part, _i}
98
- {#if part && part.startsWith("<think>")}
99
- {@const trimmed = part.trimEnd()}
100
- {@const isClosed = trimmed.endsWith("</think>")}
101
-
102
- {#if isClosed}
103
- {@const thinkContent = trimmed.slice(7, -8)}
104
- {@const summary = thinkContent.trim().split(/\n+/)[0] || "Reasoning"}
105
- <OpenReasoningResults {summary} content={thinkContent} loading={false} />
106
- {:else}
107
- <ThinkingPlaceholder />
108
- {/if}
109
- {:else if part && part.trim().length > 0}
110
- <div
111
- 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"
112
- >
113
- <MarkdownRenderer content={part} {loading} />
114
- </div>
115
  {/if}
116
- {/each}
117
- {:else}
118
  <div
119
  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"
120
  >
121
- <MarkdownRenderer content={response.content} {loading} />
122
  </div>
123
  {/if}
 
 
 
 
 
 
 
 
124
 
125
  {#if response.routerMetadata}
126
  <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
 
1
  <script lang="ts">
2
  import type { PersonaResponse } from "$lib/types/Message";
3
  import MarkdownRenderer from "./MarkdownRenderer.svelte";
 
4
  import CopyToClipBoardBtn from "../CopyToClipBoardBtn.svelte";
5
  import CarbonRotate360 from "~icons/carbon/rotate-360";
6
  import CarbonChevronDown from "~icons/carbon/chevron-down";
 
91
  class="mt-2"
92
  style={isExpanded ? '' : `max-height: ${MAX_COLLAPSED_HEIGHT}px; overflow: hidden;`}
93
  >
94
+ {#if hasClientThink(response.content)}
95
+ {@const segments = splitThinkSegments(response.content ?? "")}
96
+ {#each segments as part, _i}
97
+ {#if part && part.startsWith("<think>")}
98
+ {@const trimmed = part.trimEnd()}
99
+ {@const isClosed = trimmed.endsWith("</think>")}
100
+
101
+ {#if isClosed}
102
+ <!-- Skip closed think tags - don't show reasoning content -->
103
+ {:else}
104
+ <ThinkingPlaceholder />
 
 
 
 
 
 
 
 
 
105
  {/if}
106
+ {:else if part && part.trim().length > 0}
 
107
  <div
108
  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"
109
  >
110
+ <MarkdownRenderer content={part} {loading} />
111
  </div>
112
  {/if}
113
+ {/each}
114
+ {:else}
115
+ <div
116
+ 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"
117
+ >
118
+ <MarkdownRenderer content={response.content} {loading} />
119
+ </div>
120
+ {/if}
121
 
122
  {#if response.routerMetadata}
123
  <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
src/lib/components/chat/PersonaResponseCarousel.svelte CHANGED
@@ -5,7 +5,6 @@
5
  import CarbonChevronDown from "~icons/carbon/chevron-down";
6
  import CarbonChevronUp from "~icons/carbon/chevron-up";
7
  import MarkdownRenderer from "./MarkdownRenderer.svelte";
8
- import OpenReasoningResults from "./OpenReasoningResults.svelte";
9
  import CopyToClipBoardBtn from "../CopyToClipBoardBtn.svelte";
10
  import CarbonRotate360 from "~icons/carbon/rotate-360";
11
  import ThinkingPlaceholder from "./ThinkingPlaceholder.svelte";
@@ -273,35 +272,33 @@ import { hasThinkSegments, splitThinkSegments } from "$lib/utils/stripThinkBlock
273
  class="content-wrapper relative"
274
  style={isExpanded ? '' : `max-height: ${MAX_COLLAPSED_HEIGHT}px; overflow: hidden;`}
275
  >
276
- {#if hasClientThink(displayedResponse.content)}
277
- {@const segments = splitThinkSegments(displayedResponse.content ?? "")}
278
- {#each segments as part, _i}
279
- {#if part && part.startsWith("<think>")}
280
- {@const trimmed = part.trimEnd()}
281
- {@const isClosed = trimmed.endsWith("</think>")}
282
-
283
- {#if isClosed}
284
- {@const thinkContent = trimmed.slice(7, -8)}
285
- {@const summary = thinkContent.trim().split(/\n+/)[0] || "Reasoning"}
286
- <OpenReasoningResults {summary} content={thinkContent} loading={false} />
287
- {:else}
288
- <ThinkingPlaceholder />
289
- {/if}
290
- {:else if part && part.trim().length > 0}
291
- <div
292
- 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"
293
- >
294
- <MarkdownRenderer content={part} {loading} />
295
- </div>
296
  {/if}
297
- {/each}
298
- {:else}
299
  <div
300
  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"
301
  >
302
- <MarkdownRenderer content={displayedResponse.content} {loading} />
303
  </div>
304
  {/if}
 
 
 
 
 
 
 
 
305
 
306
  {#if displayedResponse.routerMetadata}
307
  <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
 
5
  import CarbonChevronDown from "~icons/carbon/chevron-down";
6
  import CarbonChevronUp from "~icons/carbon/chevron-up";
7
  import MarkdownRenderer from "./MarkdownRenderer.svelte";
 
8
  import CopyToClipBoardBtn from "../CopyToClipBoardBtn.svelte";
9
  import CarbonRotate360 from "~icons/carbon/rotate-360";
10
  import ThinkingPlaceholder from "./ThinkingPlaceholder.svelte";
 
272
  class="content-wrapper relative"
273
  style={isExpanded ? '' : `max-height: ${MAX_COLLAPSED_HEIGHT}px; overflow: hidden;`}
274
  >
275
+ {#if hasClientThink(displayedResponse.content)}
276
+ {@const segments = splitThinkSegments(displayedResponse.content ?? "")}
277
+ {#each segments as part, _i}
278
+ {#if part && part.startsWith("<think>")}
279
+ {@const trimmed = part.trimEnd()}
280
+ {@const isClosed = trimmed.endsWith("</think>")}
281
+
282
+ {#if isClosed}
283
+ <!-- Skip closed think tags - don't show reasoning content -->
284
+ {:else}
285
+ <ThinkingPlaceholder />
 
 
 
 
 
 
 
 
 
286
  {/if}
287
+ {:else if part && part.trim().length > 0}
 
288
  <div
289
  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"
290
  >
291
+ <MarkdownRenderer content={part} {loading} />
292
  </div>
293
  {/if}
294
+ {/each}
295
+ {:else}
296
+ <div
297
+ 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"
298
+ >
299
+ <MarkdownRenderer content={displayedResponse.content} {loading} />
300
+ </div>
301
+ {/if}
302
 
303
  {#if displayedResponse.routerMetadata}
304
  <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">