Skip to content

Commit f6f410e

Browse files
authored
feat: UI for advanced reasoning models (#1605)
* feat: add a reasoning dropdown for CoT models * feat: add status updates * fix: various cleanups - pass content & status to result dropdown - dont store streaming updates in db - make status generation non blocking * fix: make sure not to push reasoning token stream to db * feat: add time indicator and make the ui match websearch * fix: change in status update & prompt
1 parent 54306a5 commit f6f410e

File tree

16 files changed

+357
-51
lines changed

16 files changed

+357
-51
lines changed

chart/env/prod.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,9 @@ envVars:
144144
"websiteUrl": "https://qwenlm.github.io/blog/qwq-32b-preview/",
145145
"logoUrl": "https://huggingface.co/datasets/huggingchat/models-logo/resolve/main/qwen-logo.png",
146146
"description": "QwQ is an experiment model from the Qwen Team with advanced reasoning capabilities.",
147+
"reasoning": {
148+
"type": "summarize"
149+
},
147150
"parameters": {
148151
"stop": ["<|im_end|>"],
149152
"truncate": 12288,

src/lib/components/OpenWebSearchResults.svelte

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import EosIconsLoading from "~icons/eos-icons/loading";
1010
import IconInternet from "./icons/IconInternet.svelte";
1111
12-
export let classNames = "";
1312
export let webSearchMessages: MessageWebSearchUpdate[] = [];
1413
1514
$: sources = webSearchMessages.find(isMessageWebSearchSourcesUpdate)?.sources;
@@ -23,7 +22,7 @@
2322
</script>
2423

2524
<details
26-
class="flex w-fit rounded-xl border border-gray-200 bg-white shadow-sm dark:border-gray-800 dark:bg-gray-900 {classNames} max-w-full"
25+
class="flex w-fit max-w-full rounded-xl border border-gray-200 bg-white shadow-sm dark:border-gray-800 dark:bg-gray-900"
2726
>
2827
<summary class="grid min-w-72 select-none grid-cols-[40px,1fr] items-center gap-2.5 p-2">
2928
<div

src/lib/components/chat/ChatMessage.svelte

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
type MessageWebSearchSourcesUpdate,
2626
type MessageWebSearchUpdate,
2727
type MessageFinalAnswerUpdate,
28+
type MessageReasoningUpdate,
29+
MessageReasoningUpdateType,
2830
} from "$lib/types/MessageUpdate";
2931
import { base } from "$app/paths";
3032
import { useConvTreeStore } from "$lib/stores/convTree";
@@ -33,6 +35,7 @@
3335
import { enhance } from "$app/forms";
3436
import { browser } from "$app/environment";
3537
import MarkdownRenderer from "./MarkdownRenderer.svelte";
38+
import OpenReasoningResults from "./OpenReasoningResults.svelte";
3639
3740
export let model: Model;
3841
export let id: Message["id"];
@@ -90,9 +93,13 @@
9093
}
9194
}
9295
93-
$: searchUpdates = (message.updates?.filter(({ type }) => type === "webSearch") ??
96+
$: searchUpdates = (message.updates?.filter(({ type }) => type === MessageUpdateType.WebSearch) ??
9497
[]) as MessageWebSearchUpdate[];
9598
99+
$: reasoningUpdates = (message.updates?.filter(
100+
({ type }) => type === MessageUpdateType.Reasoning
101+
) ?? []) as MessageReasoningUpdate[];
102+
96103
$: messageFinalAnswer = message.updates?.find(
97104
({ type }) => type === MessageUpdateType.FinalAnswer
98105
) as MessageFinalAnswerUpdate;
@@ -208,9 +215,17 @@
208215
</div>
209216
{/if}
210217
{#if searchUpdates && searchUpdates.length > 0}
211-
<OpenWebSearchResults
212-
classNames={message.content.length ? "mb-3.5" : ""}
213-
webSearchMessages={searchUpdates}
218+
<OpenWebSearchResults webSearchMessages={searchUpdates} />
219+
{/if}
220+
{#if reasoningUpdates && reasoningUpdates.length > 0}
221+
{@const summaries = reasoningUpdates
222+
.filter((u) => u.subtype === MessageReasoningUpdateType.Status)
223+
.map((u) => u.status)}
224+
225+
<OpenReasoningResults
226+
summary={summaries[summaries.length - 1] || ""}
227+
content={message.reasoning || ""}
228+
loading={loading && message.content.length === 0}
214229
/>
215230
{/if}
216231

@@ -224,11 +239,19 @@
224239
{/each}
225240
{/if}
226241

227-
<div bind:this={contentEl}>
242+
<div
243+
bind:this={contentEl}
244+
class:mt-2={reasoningUpdates.length > 0 || searchUpdates.length > 0}
245+
>
228246
{#if isLast && loading && $settings.disableStream}
229247
<IconLoading classNames="loading inline ml-2 first:ml-0" />
230248
{/if}
231-
<MarkdownRenderer content={message.content} sources={webSearchSources} />
249+
250+
<div
251+
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"
252+
>
253+
<MarkdownRenderer content={message.content} sources={webSearchSources} />
254+
</div>
232255
</div>
233256

234257
<!-- Web Search sources -->

src/lib/components/chat/MarkdownRenderer.svelte

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -106,21 +106,17 @@
106106
});
107107
</script>
108108

109-
<div
110-
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"
111-
>
112-
{#each tokens as token}
113-
{#if token.type === "code"}
114-
<CodeBlock lang={token.lang} code={token.text} />
115-
{:else}
116-
{@const parsed = marked.parse(processLatex(escapeHTML(token.raw)), options)}
117-
{#await parsed then parsed}
118-
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
119-
{@html DOMPurify.sanitize(parsed)}
120-
{/await}
121-
{/if}
122-
{/each}
123-
</div>
109+
{#each tokens as token}
110+
{#if token.type === "code"}
111+
<CodeBlock lang={token.lang} code={token.text} />
112+
{:else}
113+
{@const parsed = marked.parse(processLatex(escapeHTML(token.raw)), options)}
114+
{#await parsed then parsed}
115+
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
116+
{@html DOMPurify.sanitize(parsed)}
117+
{/await}
118+
{/if}
119+
{/each}
124120

125121
<style lang="postcss">
126122
:global(.katex-display) {
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<script lang="ts">
2+
import IconThought from "~icons/carbon/circle-packing";
3+
import MarkdownRenderer from "./MarkdownRenderer.svelte";
4+
5+
export let summary: string;
6+
export let content: string;
7+
export let loading: boolean = false;
8+
</script>
9+
10+
<details
11+
class="u flex w-fit max-w-full rounded-xl border border-gray-200 bg-white shadow-sm dark:border-gray-800 dark:bg-gray-900"
12+
>
13+
<summary
14+
class="grid min-w-72 cursor-pointer select-none grid-cols-[40px,1fr] items-center gap-2.5 p-2"
15+
>
16+
<div
17+
class="relative grid aspect-square place-content-center overflow-hidden rounded-lg bg-gray-100 dark:bg-gray-800"
18+
>
19+
<svg
20+
class="absolute inset-0 text-gray-300 transition-opacity dark:text-gray-700 {loading
21+
? 'opacity-100'
22+
: 'opacity-0'}"
23+
width="40"
24+
height="40"
25+
viewBox="0 0 38 38"
26+
fill="none"
27+
xmlns="http://www.w3.org/2000/svg"
28+
>
29+
<path
30+
class="loading-path"
31+
d="M8 2.5H30C30 2.5 35.5 2.5 35.5 8V30C35.5 30 35.5 35.5 30 35.5H8C8 35.5 2.5 35.5 2.5 30V8C2.5 8 2.5 2.5 8 2.5Z"
32+
stroke="currentColor"
33+
stroke-width="1"
34+
stroke-linecap="round"
35+
id="shape"
36+
/>
37+
</svg>
38+
39+
<IconThought class="text-[1rem]" />
40+
</div>
41+
<dl class="leading-4">
42+
<dd class="text-sm">Reasoning</dd>
43+
<dt
44+
class="flex items-center gap-1 truncate whitespace-nowrap text-[.82rem] text-gray-400"
45+
class:animate-pulse={loading}
46+
>
47+
{summary}
48+
</dt>
49+
</dl>
50+
</summary>
51+
52+
<div
53+
class="border-t border-gray-200 px-5 pb-2 pt-2 text-sm text-gray-600 dark:border-gray-800 dark:text-gray-400"
54+
>
55+
<MarkdownRenderer {content} />
56+
</div>
57+
</details>
58+
59+
<style>
60+
details summary::-webkit-details-marker {
61+
display: none;
62+
}
63+
64+
.loading-path {
65+
stroke-dasharray: 61.45;
66+
animation: loading 2s linear infinite;
67+
}
68+
69+
@keyframes loading {
70+
to {
71+
stroke-dashoffset: 122.9;
72+
}
73+
}
74+
</style>

src/lib/server/generateFromDefaultEndpoint.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import { smallModel } from "$lib/server/models";
2+
import { MessageUpdateType, type MessageUpdate } from "$lib/types/MessageUpdate";
23
import type { EndpointMessage } from "./endpoints/endpoints";
34

4-
export async function generateFromDefaultEndpoint({
5+
export async function* generateFromDefaultEndpoint({
56
messages,
67
preprompt,
78
generateSettings,
89
}: {
910
messages: EndpointMessage[];
1011
preprompt?: string;
1112
generateSettings?: Record<string, unknown>;
12-
}): Promise<string> {
13+
}): AsyncGenerator<MessageUpdate, string, undefined> {
1314
const endpoint = await smallModel.getEndpoint();
1415

1516
const tokenStream = await endpoint({ messages, preprompt, generateSettings });
@@ -25,6 +26,10 @@ export async function generateFromDefaultEndpoint({
2526
}
2627
return generated_text;
2728
}
29+
yield {
30+
type: MessageUpdateType.Stream,
31+
token: output.token.text,
32+
};
2833
}
2934
throw new Error("Generation failed");
3035
}

src/lib/server/models.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,21 @@ import { isHuggingChat } from "$lib/utils/isHuggingChat";
1717

1818
type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
1919

20+
const reasoningSchema = z.union([
21+
z.object({
22+
type: z.literal("regex"), // everything is reasoning, extract the answer from the regex
23+
regex: z.string(),
24+
}),
25+
z.object({
26+
type: z.literal("tokens"), // use beginning and end tokens that define the reasoning portion of the answer
27+
beginToken: z.string(),
28+
endToken: z.string(),
29+
}),
30+
z.object({
31+
type: z.literal("summarize"), // everything is reasoning, summarize the answer
32+
}),
33+
]);
34+
2035
const modelConfig = z.object({
2136
/** Used as an identifier in DB */
2237
id: z.string().optional(),
@@ -70,6 +85,7 @@ const modelConfig = z.object({
7085
embeddingModel: validateEmbeddingModelByName(embeddingModels).optional(),
7186
/** Used to enable/disable system prompt usage */
7287
systemRoleSupported: z.boolean().default(true),
88+
reasoning: reasoningSchema.optional(),
7389
});
7490

7591
const modelsRaw = z.array(modelConfig).parse(JSON5.parse(env.MODELS));

0 commit comments

Comments
 (0)