Skip to content

Commit 52fe8c9

Browse files
committed
[UI] add support for reasoning model in tables and chat (#821)
1 parent 8dfa0c5 commit 52fe8c9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+9336
-7221
lines changed

services/app/package-lock.json

100755100644
Lines changed: 7201 additions & 6410 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

services/app/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"@electron-forge/maker-zip": "^7.4.0",
3535
"@faker-js/faker": "^8.4.1",
3636
"@inlang/cli": "^3.0.0",
37-
"@inlang/paraglide-js": "2.0.13",
37+
"@inlang/paraglide-js": "^2.5.0",
3838
"@lucide/svelte": "^0.482.0",
3939
"@playwright/test": "^1.28.1",
4040
"@sveltejs/adapter-node": "^5.0.1",

services/app/src/hooks.server.ts

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import logger from '$lib/logger';
55
import { paraglideMiddleware } from '$lib/paraglide/server';
66
import { getPrices } from '$lib/server/nodeCache';
77
import type { Auth0User, User } from '$lib/types';
8-
import type { Session } from '@auth/sveltekit';
98
import { error, redirect, type Handle } from '@sveltejs/kit';
109
import { sequence } from '@sveltejs/kit/hooks';
1110
import { Agent } from 'undici';
@@ -66,11 +65,9 @@ const handleApiProxy: Handle = async ({ event }) => {
6665

6766
event.request.headers.delete('connection');
6867

69-
if (event.locals.user) {
70-
if (!ossMode) {
71-
event.request.headers.append('Authorization', `Bearer ${OWL_SERVICE_KEY}`);
72-
}
73-
event.request.headers.append('x-user-id', event.locals.user.id);
68+
if (event.locals.user || ossMode) {
69+
event.request.headers.append('Authorization', `Bearer ${OWL_SERVICE_KEY}`);
70+
event.request.headers.append('x-user-id', event.locals.user?.id ?? '0');
7471
}
7572

7673
if (!event.request.headers.get('x-project-id') && event.cookies.get('activeProjectId')) {
@@ -111,7 +108,6 @@ export const mainHandle: Handle = async ({ event, resolve }) => {
111108
locals.auth0Mode = auth0Mode;
112109

113110
let auth0UserData: Auth0User;
114-
let session: Session | null;
115111
if (auth0Mode) {
116112
//? Workaround for event.platform unavailable in development
117113
if (dev) {
@@ -125,14 +121,12 @@ export const mainHandle: Handle = async ({ event, resolve }) => {
125121
// @ts-expect-error missing type
126122
auth0UserData = event.platform?.req?.res?.locals?.user;
127123
}
128-
} else {
129-
session = await locals.auth();
130124
}
131125

132126
//@ts-expect-error asd
133-
if (auth0UserData || session) {
127+
if (auth0UserData || ossMode) {
134128
//@ts-expect-error asd
135-
let userApiData = await getUserApiData(auth0UserData?.sub ?? session?.user?.id);
129+
let userApiData = await getUserApiData(auth0UserData?.sub ?? '0');
136130
if (!userApiData.data) {
137131
if (auth0Mode && userApiData.status === 404) {
138132
const userUpsertRes = await fetch(`${OWL_URL}/api/v2/users`, {
@@ -199,6 +193,7 @@ export const mainHandle: Handle = async ({ event, resolve }) => {
199193
} */
200194

201195
if (
196+
!ossMode &&
202197
!url.pathname.startsWith('/api') &&
203198
!url.pathname.startsWith('/login') &&
204199
!url.pathname.startsWith('/register')

services/app/src/lib/components/chat/ChatFilePreview.svelte

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
<script lang="ts">
22
import { PUBLIC_JAMAI_URL } from '$env/static/public';
3+
import { AudioLines } from '@lucide/svelte';
34
import * as PDFObject from 'pdfobject';
45
import csv from 'csvtojson';
5-
import showdown from 'showdown';
6-
//@ts-expect-error - no types
7-
import showdownHtmlEscape from 'showdown-htmlescape';
8-
import '../../../showdown-theme.css';
9-
import { codeblock, codehighlight, table as tableExtension } from '$lib/showdown';
10-
import { AudioLines } from '@lucide/svelte';
6+
import converter from '$lib/showdown';
117
import logger from '$lib/logger';
128
import { fileColumnFiletypes } from '$lib/constants';
139
@@ -50,15 +46,6 @@
5046
}
5147
}
5248
53-
const converter = new showdown.Converter({
54-
tables: true,
55-
tasklists: true,
56-
disableForced4SpacesIndentedSublists: true,
57-
strikethrough: true,
58-
ghCompatibleHeaderId: true,
59-
extensions: [showdownHtmlEscape, codeblock, codehighlight, tableExtension]
60-
});
61-
6249
async function loadPreview() {
6350
if (!fileUrl) return;
6451
const response = await fetch(fileUrl);
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
<script lang="ts">
2+
import { page } from '$app/state';
3+
import { ChevronRight } from '@lucide/svelte';
4+
import converter from '$lib/showdown';
5+
import { chatCitationPattern } from '$lib/constants';
6+
import { citationReplacer } from '$lib/utils';
7+
8+
import References from './References.svelte';
9+
import { ChatState } from '../../../routes/(main)/chat/chat.svelte';
10+
import { TableState } from '$lib/components/tables/tablesState.svelte';
11+
import CloseIcon from '$lib/icons/CloseIcon.svelte';
12+
13+
let {
14+
showOutputDetails = $bindable()
15+
}: {
16+
showOutputDetails: TableState['showOutputDetails'] | ChatState['showOutputDetails'];
17+
} = $props();
18+
19+
let tabItems = $derived(
20+
[
21+
{
22+
id: 'answer',
23+
title: 'Answer',
24+
condition: true
25+
},
26+
{
27+
id: 'thinking',
28+
title: 'Thinking',
29+
condition: !!showOutputDetails.reasoningContent
30+
},
31+
{
32+
id: 'references',
33+
title: 'References',
34+
condition: (showOutputDetails.message?.chunks.length ?? 0) > 0
35+
}
36+
].filter((t) => t.condition)
37+
);
38+
let tabHighlightPos = $derived(
39+
(tabItems.findIndex((t) => showOutputDetails.activeTab === t.id) / tabItems.length) * 100
40+
);
41+
42+
function handleCustomBtnClick(e: MouseEvent) {
43+
if (page.url.pathname.startsWith('/chat')) return;
44+
45+
const target = e.target as HTMLElement;
46+
if (target.classList.contains('citation-btn')) {
47+
const columnID = target.getAttribute('data-column');
48+
const rowID = target.getAttribute('data-row');
49+
const chunkID = target.getAttribute('data-citation');
50+
if (columnID && rowID && chunkID) {
51+
showOutputDetails = {
52+
...showOutputDetails,
53+
open: true,
54+
activeTab: 'references',
55+
expandChunk: chunkID,
56+
preview: null
57+
};
58+
}
59+
}
60+
}
61+
</script>
62+
63+
<svelte:document onclick={handleCustomBtnClick} />
64+
65+
<div class="z-[1] h-full pb-4 pl-3 pr-3 md:pl-0">
66+
<div
67+
style="box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.08);"
68+
class="flex h-full flex-col rounded-lg border border-[#E4E7EC] bg-white"
69+
>
70+
<div
71+
class="relative flex h-min items-center justify-between space-y-1.5 rounded-t-lg bg-white px-4 py-3 text-left text-lg font-medium text-[#344054] data-dark:bg-[#303338]"
72+
>
73+
<!-- {#if showOutputDetails.preview}
74+
<div class="flex items-center gap-2">
75+
<button
76+
onclick={() => (showOutputDetails = { ...showOutputDetails, preview: null })}
77+
class="flex aspect-square h-8 items-center justify-center rounded-full !bg-transparent ring-offset-background transition-colors hover:!bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-black"
78+
>
79+
<ArrowBackIcon class="w-6 [&>*]:stroke-[#1D2939]" />
80+
</button>
81+
82+
<span class="line-clamp-1">
83+
{showOutputDetails.preview.document_id.split('/').pop()}
84+
</span>
85+
</div>
86+
{:else}
87+
Citations
88+
{/if} -->
89+
Output details
90+
91+
<button
92+
onclick={() => (showOutputDetails = { ...showOutputDetails, open: false, preview: null })}
93+
class="flex aspect-square h-8 items-center justify-center rounded-full !bg-transparent ring-offset-background transition-colors hover:!bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-black"
94+
>
95+
<CloseIcon class="w-6" />
96+
<span class="sr-only">Close</span>
97+
</button>
98+
</div>
99+
100+
<div
101+
data-testid="output-details-tabs"
102+
style="grid-template-columns: repeat({tabItems.length}, minmax(6rem, 1fr));"
103+
class="relative grid w-full items-end overflow-auto border-b border-[#F2F4F7] text-xs sm:text-sm"
104+
>
105+
{#each tabItems as { id, title, condition }}
106+
{#if condition}
107+
<button
108+
onclick={() => (showOutputDetails.activeTab = id)}
109+
class="px-0 py-2 font-medium sm:px-4 {showOutputDetails.activeTab === id
110+
? 'text-[#344054]'
111+
: 'text-[#98A2B3]'} text-center transition-colors"
112+
>
113+
{title}
114+
</button>
115+
{/if}
116+
{/each}
117+
118+
<div
119+
style="width: {(1 / tabItems.length) * 100}%; left: {tabHighlightPos}%;"
120+
class="absolute bottom-0 h-[3px] bg-secondary transition-[left]"
121+
></div>
122+
</div>
123+
124+
{#if showOutputDetails.activeTab === 'answer'}
125+
{@const rawHtml = converter
126+
.makeHtml(showOutputDetails.message?.content ?? '')
127+
.replaceAll(chatCitationPattern, (match, word) =>
128+
citationReplacer(
129+
match,
130+
word,
131+
showOutputDetails.activeCell?.columnID ?? '',
132+
showOutputDetails.activeCell?.rowID ?? '',
133+
showOutputDetails.message?.chunks ?? []
134+
)
135+
)}
136+
137+
<div class="flex h-1 grow flex-col items-center gap-2 overflow-auto px-8 py-4">
138+
<p class="response-message flex max-w-full flex-col gap-4 whitespace-pre-line text-sm">
139+
{@html rawHtml}
140+
</p>
141+
</div>
142+
{:else if showOutputDetails.activeTab === 'thinking'}
143+
{@const rawHtml = converter.makeHtml(showOutputDetails.reasoningContent ?? '')}
144+
<div class="flex h-1 grow flex-col items-center gap-2 overflow-auto px-8 py-4">
145+
{#if showOutputDetails.reasoningTime}
146+
<div class="mb-2 flex select-none items-center gap-2 self-start text-sm text-[#667085]">
147+
<ChevronRight size={16} />
148+
Thought for {showOutputDetails.reasoningTime.toFixed()} second{Number(
149+
showOutputDetails.reasoningTime.toFixed()
150+
) > 1
151+
? 's'
152+
: ''}
153+
</div>
154+
{/if}
155+
156+
<p
157+
class="response-message flex max-w-full flex-col gap-4 whitespace-pre-line text-sm text-[#475467]"
158+
>
159+
{@html rawHtml}
160+
</p>
161+
</div>
162+
{:else if showOutputDetails.activeTab === 'references'}
163+
<References bind:showReferences={showOutputDetails} />
164+
{/if}
165+
</div>
166+
</div>
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<script lang="ts">
2+
import { getTableState, TableState } from '$lib/components/tables/tablesState.svelte';
3+
import OutputDetails from '$lib/components/output-details/OutputDetails.svelte';
4+
import type { ChatState } from '../../../routes/(main)/chat/chat.svelte';
5+
6+
let {
7+
showOutputDetails = $bindable()
8+
}: {
9+
showOutputDetails: TableState['showOutputDetails'] | ChatState['showOutputDetails'];
10+
} = $props();
11+
12+
const tableState = getTableState();
13+
14+
let showActual = $state(tableState.showOutputDetails.open);
15+
16+
function closeOutputDetails() {
17+
tableState.showOutputDetails = { ...tableState.showOutputDetails, open: false };
18+
}
19+
</script>
20+
21+
<!-- Column settings barrier dismissable -->
22+
<!-- svelte-ignore a11y_click_events_have_key_events -->
23+
<!-- svelte-ignore a11y_no_static_element_interactions -->
24+
<div
25+
class="absolute inset-0 z-30 {tableState.showOutputDetails.open
26+
? 'pointer-events-auto opacity-100'
27+
: 'pointer-events-none opacity-0'} transition-opacity duration-300"
28+
onclick={closeOutputDetails}
29+
></div>
30+
31+
{#if tableState.showOutputDetails.open || showActual}
32+
<div
33+
data-testid="output-details-area"
34+
inert={!tableState.showOutputDetails.open}
35+
onanimationstart={() => {
36+
if (tableState.showOutputDetails.open) {
37+
showActual = true;
38+
}
39+
}}
40+
onanimationend={() => {
41+
if (!tableState.showOutputDetails.open) {
42+
showActual = false;
43+
}
44+
}}
45+
class="output-details fixed bottom-0 right-0 z-40 h-[clamp(0px,85%,100%)] px-4 py-3 {tableState
46+
.showOutputDetails.open
47+
? 'animate-in slide-in-from-right-full'
48+
: 'animate-out slide-out-to-right-full'} duration-300 ease-in-out"
49+
>
50+
<OutputDetails bind:showOutputDetails />
51+
</div>
52+
{/if}
53+
54+
<style>
55+
.output-details {
56+
width: 100%;
57+
}
58+
59+
@media (min-width: 800px) {
60+
.output-details {
61+
width: calc(100% * 5 / 6);
62+
}
63+
}
64+
65+
@media (min-width: 1000px) {
66+
.output-details {
67+
width: calc(100% * 4 / 6);
68+
}
69+
}
70+
71+
@media (min-width: 1200px) {
72+
.output-details {
73+
width: 50%;
74+
}
75+
}
76+
</style>

services/app/src/routes/(main)/chat/[project_id]/[conversation_id]/(components)/PDFViewer.svelte renamed to services/app/src/lib/components/output-details/PDFViewer.svelte

File renamed without changes.

0 commit comments

Comments
 (0)