Skip to content

Commit fedbe8b

Browse files
committed
[UI] fix chat mode & other fixes (#858)
1 parent ed3ba82 commit fedbe8b

File tree

27 files changed

+2048
-1164
lines changed

27 files changed

+2048
-1164
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
method: 'POST',
2525
headers: {
2626
'Content-Type': 'application/json',
27-
'x-project-id': page.params.project_id
27+
'x-project-id': page.params.project_id ?? ''
2828
},
2929
body: JSON.stringify({
3030
uris: rowThumbsUris

services/app/src/lib/components/output-details/OutputDetails.svelte

Lines changed: 216 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,48 @@
11
<script lang="ts">
2+
import { PUBLIC_JAMAI_URL } from '$env/static/public';
3+
import { ArrowDownToLine, Sparkle, Trash2 } from '@lucide/svelte';
24
import { page } from '$app/state';
3-
import { Sparkle } from '@lucide/svelte';
45
import converter from '$lib/showdown';
56
import { chatCitationPattern } from '$lib/constants';
67
import { citationReplacer } from '$lib/utils';
8+
import logger from '$lib/logger';
9+
import type { GenTableCol, GenTableRow } from '$lib/types';
710
811
import References from './References.svelte';
9-
import { ChatState } from '../../../routes/(main)/chat/chat.svelte';
10-
import { TableState } from '$lib/components/tables/tablesState.svelte';
12+
import { chatState, ChatState } from '../../../routes/(main)/chat/chat.svelte';
13+
import { Button } from '$lib/components/ui/button';
14+
import { ColumnTypeTag } from '$lib/components/tables/(sub)';
15+
import { CustomToastDesc, toast } from '$lib/components/ui/sonner';
16+
import {
17+
getTableState,
18+
getTableRowsState,
19+
TableState
20+
} from '$lib/components/tables/tablesState.svelte';
1121
import CloseIcon from '$lib/icons/CloseIcon.svelte';
22+
import MultiturnChatIcon from '$lib/icons/MultiturnChatIcon.svelte';
23+
24+
const tableState = getTableState();
25+
const tableRowsState = getTableRowsState();
1226
1327
let {
1428
showOutputDetails = $bindable()
1529
}: {
1630
showOutputDetails: TableState['showOutputDetails'] | ChatState['showOutputDetails'];
1731
} = $props();
1832
33+
let column = $derived(
34+
(page.url.pathname.startsWith('/chat')
35+
? chatState.conversation?.cols
36+
: tableState.tableData?.cols
37+
)?.find((c) => c.id === showOutputDetails.activeCell?.columnID)
38+
);
39+
let colType = $derived(!column?.gen_config ? 'input' : 'output');
40+
1941
let tabItems = $derived(
2042
[
2143
{
2244
id: 'answer',
23-
title: 'Answer',
45+
title: colType === 'input' ? 'Input' : 'Answer',
2446
condition: true
2547
},
2648
{
@@ -58,17 +80,120 @@
5880
}
5981
}
6082
}
83+
84+
async function getRawFile(fileUri: string) {
85+
if (!fileUri) return;
86+
87+
const response = await fetch(`${PUBLIC_JAMAI_URL}/api/owl/files/url/raw`, {
88+
method: 'POST',
89+
headers: {
90+
'Content-Type': 'application/json'
91+
},
92+
body: JSON.stringify({
93+
uris: [fileUri]
94+
})
95+
});
96+
const responseBody = await response.json();
97+
98+
if (response.ok) {
99+
window.open(responseBody.urls[0], '_blank');
100+
} else {
101+
if (response.status !== 404) {
102+
logger.error('GETRAW', responseBody);
103+
}
104+
toast.error('Failed to get raw file', {
105+
id: responseBody.message || JSON.stringify(responseBody),
106+
description: CustomToastDesc as any,
107+
componentProps: {
108+
description: responseBody.message || JSON.stringify(responseBody),
109+
requestID: responseBody.request_id
110+
}
111+
});
112+
}
113+
}
61114
</script>
62115

63-
<svelte:document onclick={handleCustomBtnClick} />
116+
<svelte:document
117+
onclick={handleCustomBtnClick}
118+
onkeydown={(e) => {
119+
if (
120+
page.url.pathname.startsWith('/chat') ||
121+
!tableRowsState.rows ||
122+
!tableState.tableData ||
123+
!column
124+
)
125+
return;
126+
127+
const key = e.key;
128+
if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(key)) return;
129+
130+
const rows = tableRowsState.rows;
131+
const cols = tableState.tableData.cols;
132+
const currentRowID = showOutputDetails.activeCell?.rowID;
133+
const currentColID = column?.id;
134+
135+
function setDetails(row: GenTableRow | undefined, col: GenTableCol) {
136+
if (!row || !col) return;
137+
const cell = row[col.id] ?? {};
138+
const value = cell.value ?? '';
139+
const chunks = cell.references?.chunks ?? [];
140+
showOutputDetails = {
141+
open: true,
142+
activeCell: { rowID: row.ID, columnID: col.id },
143+
activeTab:
144+
col.dtype === 'image'
145+
? 'image'
146+
: tableState.streamingRows[row.ID]?.includes(col.id) && !value
147+
? 'thinking'
148+
: 'answer',
149+
message: { content: value, chunks, fileUrl: tableState.rowThumbs[row.ID]?.[col.id]?.url },
150+
reasoningContent: cell.reasoning_content ?? null,
151+
reasoningTime: cell.reasoning_time ?? null,
152+
expandChunk: null,
153+
preview: null
154+
};
155+
}
156+
157+
const rowIndex = rows.findIndex((r) => r.ID === currentRowID);
158+
const colIndex = cols.findIndex((c) => c.id === currentColID);
159+
160+
switch (key) {
161+
case 'ArrowUp':
162+
if (rowIndex > 0) setDetails(rows[rowIndex - 1], column);
163+
break;
164+
165+
case 'ArrowDown':
166+
if (rowIndex !== -1 && rowIndex < rows.length - 1) setDetails(rows[rowIndex + 1], column);
167+
break;
168+
169+
case 'ArrowLeft': {
170+
if (colIndex > 2) {
171+
const prevCol = cols[colIndex - 1];
172+
const currentRow = rows.find((r) => r.ID === currentRowID);
173+
setDetails(currentRow, prevCol);
174+
}
175+
break;
176+
}
177+
178+
case 'ArrowRight': {
179+
if (colIndex !== -1 && colIndex < cols.length - 1) {
180+
const nextCol = cols[colIndex + 1];
181+
const currentRow = rows.find((r) => r.ID === currentRowID);
182+
setDetails(currentRow, nextCol);
183+
}
184+
break;
185+
}
186+
}
187+
}}
188+
/>
64189

65190
<div class="z-[1] h-full pb-4 pl-3 pr-3 md:pl-0">
66191
<div
67192
style="box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.08);"
68193
class="flex h-full flex-col rounded-lg border border-[#E4E7EC] bg-white"
69194
>
70195
<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]"
196+
class="relative flex h-min items-center justify-between space-y-1.5 rounded-t-lg bg-white px-4 py-3 data-dark:bg-[#303338]"
72197
>
73198
<!-- {#if showOutputDetails.preview}
74199
<div class="flex items-center gap-2">
@@ -86,7 +211,23 @@
86211
{:else}
87212
Citations
88213
{/if} -->
89-
Output details
214+
215+
{#if showOutputDetails.activeCell}
216+
<div class="flex items-center gap-2">
217+
{#if column}
218+
<ColumnTypeTag
219+
colType={!column.gen_config ? 'input' : 'output'}
220+
dtype={column.dtype}
221+
columnID={column.id}
222+
genConfig={column.gen_config}
223+
/>
224+
{/if}
225+
226+
<p class="text-[#667085]">{showOutputDetails.activeCell?.columnID}</p>
227+
</div>
228+
{:else}
229+
<p class="text-left text-lg font-medium text-[#344054]">Output details</p>
230+
{/if}
90231

91232
<button
92233
onclick={() => (showOutputDetails = { ...showOutputDetails, open: false, preview: null })}
@@ -97,31 +238,74 @@
97238
</button>
98239
</div>
99240

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-
241+
{#if showOutputDetails.activeTab !== 'image'}
118242
<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>
243+
data-testid="output-details-tabs"
244+
style="grid-template-columns: repeat({tabItems.length}, minmax(6rem, 1fr));"
245+
class="relative grid w-fit items-end overflow-auto border-b border-[#F2F4F7] text-xs sm:text-sm"
246+
>
247+
{#each tabItems as { id, title, condition }}
248+
{#if condition}
249+
<button
250+
onclick={() => (showOutputDetails.activeTab = id)}
251+
class="px-0 py-2 font-medium sm:px-4 {showOutputDetails.activeTab === id
252+
? 'text-[#344054]'
253+
: 'text-[#98A2B3]'} text-center transition-colors"
254+
>
255+
{title}
256+
</button>
257+
{/if}
258+
{/each}
123259

124-
{#if showOutputDetails.activeTab === 'answer'}
260+
<div
261+
style="width: {(1 / tabItems.length) * 100}%; left: {tabHighlightPos}%;"
262+
class="absolute bottom-0 h-[3px] bg-secondary transition-[left]"
263+
></div>
264+
</div>
265+
{/if}
266+
267+
{#if showOutputDetails.activeTab === 'image'}
268+
<div class="flex h-1 grow flex-col gap-2 p-3">
269+
<img src={showOutputDetails.message?.fileUrl} alt="" class="max-h-[45vh] object-contain" />
270+
271+
<p
272+
title={showOutputDetails.message?.content.split('/').pop()}
273+
class="break-all rounded text-sm text-[#667085]"
274+
>
275+
{showOutputDetails.message?.content.split('/').pop()}
276+
</p>
277+
278+
<div class="flex gap-1">
279+
<Button
280+
variant="ghost"
281+
title="Download file"
282+
onclick={() => getRawFile(showOutputDetails.message?.content ?? '')}
283+
class="h-8 w-max gap-2 rounded-md border border-[#E4E7EC] bg-white px-2 font-normal text-[#667085] shadow-[0px_1px_3px_0px_rgba(16,24,40,0.1)] hover:bg-[#F9FAFB] hover:text-[#667085]"
284+
>
285+
<ArrowDownToLine class="h-3.5 w-3.5" />
286+
Download
287+
</Button>
288+
289+
{#if showOutputDetails.activeCell?.rowID}
290+
<Button
291+
variant="ghost"
292+
title="Delete file"
293+
onclick={() => {
294+
tableState.deletingFile = {
295+
rowID: showOutputDetails.activeCell?.rowID ?? '',
296+
columnID: showOutputDetails.activeCell?.columnID ?? '',
297+
fileUri: showOutputDetails.message?.content ?? ''
298+
};
299+
}}
300+
class="h-8 w-max gap-2 rounded-md border border-[#E4E7EC] bg-white px-2 font-normal text-[#F04438] shadow-[0px_1px_3px_0px_rgba(16,24,40,0.1)] hover:bg-[#F9FAFB] hover:text-[#F04438]"
301+
>
302+
<Trash2 class="h-3.5 w-3.5" />
303+
Delete
304+
</Button>
305+
{/if}
306+
</div>
307+
</div>
308+
{:else if showOutputDetails.activeTab === 'answer'}
125309
{@const rawHtml = converter
126310
.makeHtml(showOutputDetails.message?.content ?? '')
127311
.replaceAll(chatCitationPattern, (match, word) =>
@@ -134,14 +318,14 @@
134318
)
135319
)}
136320

137-
<div class="flex h-1 grow flex-col items-center gap-2 overflow-auto px-8 py-4">
321+
<div class="flex h-1 grow flex-col gap-2 overflow-auto px-8 py-4">
138322
<p class="response-message flex max-w-full flex-col gap-4 whitespace-pre-line text-sm">
139323
{@html rawHtml}
140324
</p>
141325
</div>
142326
{:else if showOutputDetails.activeTab === 'thinking'}
143327
{@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">
328+
<div class="flex h-1 grow flex-col gap-2 overflow-auto px-8 py-4">
145329
{#if showOutputDetails.reasoningTime}
146330
<div class="mb-2 flex select-none items-center gap-2 self-start text-sm text-[#667085]">
147331
<Sparkle size={16} />

services/app/src/lib/components/output-details/OutputDetailsWrapper.svelte

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script lang="ts">
2-
import { getTableState, TableState } from '$lib/components/tables/tablesState.svelte';
2+
import type { TableState } from '$lib/components/tables/tablesState.svelte';
33
import OutputDetails from '$lib/components/output-details/OutputDetails.svelte';
44
import type { ChatState } from '../../../routes/(main)/chat/chat.svelte';
55
@@ -9,41 +9,38 @@
99
showOutputDetails: TableState['showOutputDetails'] | ChatState['showOutputDetails'];
1010
} = $props();
1111
12-
const tableState = getTableState();
13-
14-
let showActual = $state(tableState.showOutputDetails.open);
12+
let showActual = $state(showOutputDetails.open);
1513
1614
function closeOutputDetails() {
17-
tableState.showOutputDetails = { ...tableState.showOutputDetails, open: false };
15+
showOutputDetails = { ...showOutputDetails, open: false };
1816
}
1917
</script>
2018

21-
<!-- Column settings barrier dismissable -->
19+
<!-- barrier dismissable -->
2220
<!-- svelte-ignore a11y_click_events_have_key_events -->
2321
<!-- svelte-ignore a11y_no_static_element_interactions -->
2422
<div
25-
class="absolute inset-0 z-30 {tableState.showOutputDetails.open
23+
class="absolute inset-0 z-30 {showOutputDetails.open
2624
? 'pointer-events-auto opacity-100'
2725
: 'pointer-events-none opacity-0'} transition-opacity duration-300"
2826
onclick={closeOutputDetails}
2927
></div>
3028

31-
{#if tableState.showOutputDetails.open || showActual}
29+
{#if showOutputDetails.open || showActual}
3230
<div
3331
data-testid="output-details-area"
34-
inert={!tableState.showOutputDetails.open}
32+
inert={!showOutputDetails.open}
3533
onanimationstart={() => {
36-
if (tableState.showOutputDetails.open) {
34+
if (showOutputDetails.open) {
3735
showActual = true;
3836
}
3937
}}
4038
onanimationend={() => {
41-
if (!tableState.showOutputDetails.open) {
39+
if (!showOutputDetails.open) {
4240
showActual = false;
4341
}
4442
}}
45-
class="output-details fixed bottom-0 right-0 z-40 h-[clamp(0px,85%,100%)] px-4 py-3 {tableState
46-
.showOutputDetails.open
43+
class="output-details fixed bottom-0 right-0 z-40 h-[clamp(0px,88%,100%)] px-4 py-3 {showOutputDetails.open
4744
? 'animate-in slide-in-from-right-full'
4845
: 'animate-out slide-out-to-right-full'} duration-300 ease-in-out"
4946
>

0 commit comments

Comments
 (0)