|
1 | 1 | <script lang="ts"> |
| 2 | + import { PUBLIC_JAMAI_URL } from '$env/static/public'; |
| 3 | + import { ArrowDownToLine, Sparkle, Trash2 } from '@lucide/svelte'; |
2 | 4 | import { page } from '$app/state'; |
3 | | - import { Sparkle } from '@lucide/svelte'; |
4 | 5 | import converter from '$lib/showdown'; |
5 | 6 | import { chatCitationPattern } from '$lib/constants'; |
6 | 7 | import { citationReplacer } from '$lib/utils'; |
| 8 | + import logger from '$lib/logger'; |
| 9 | + import type { GenTableCol, GenTableRow } from '$lib/types'; |
7 | 10 |
|
8 | 11 | 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'; |
11 | 21 | import CloseIcon from '$lib/icons/CloseIcon.svelte'; |
| 22 | + import MultiturnChatIcon from '$lib/icons/MultiturnChatIcon.svelte'; |
| 23 | +
|
| 24 | + const tableState = getTableState(); |
| 25 | + const tableRowsState = getTableRowsState(); |
12 | 26 |
|
13 | 27 | let { |
14 | 28 | showOutputDetails = $bindable() |
15 | 29 | }: { |
16 | 30 | showOutputDetails: TableState['showOutputDetails'] | ChatState['showOutputDetails']; |
17 | 31 | } = $props(); |
18 | 32 |
|
| 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 | +
|
19 | 41 | let tabItems = $derived( |
20 | 42 | [ |
21 | 43 | { |
22 | 44 | id: 'answer', |
23 | | - title: 'Answer', |
| 45 | + title: colType === 'input' ? 'Input' : 'Answer', |
24 | 46 | condition: true |
25 | 47 | }, |
26 | 48 | { |
|
58 | 80 | } |
59 | 81 | } |
60 | 82 | } |
| 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 | + } |
61 | 114 | </script> |
62 | 115 |
|
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 | +/> |
64 | 189 |
|
65 | 190 | <div class="z-[1] h-full pb-4 pl-3 pr-3 md:pl-0"> |
66 | 191 | <div |
67 | 192 | style="box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.08);" |
68 | 193 | class="flex h-full flex-col rounded-lg border border-[#E4E7EC] bg-white" |
69 | 194 | > |
70 | 195 | <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]" |
72 | 197 | > |
73 | 198 | <!-- {#if showOutputDetails.preview} |
74 | 199 | <div class="flex items-center gap-2"> |
|
86 | 211 | {:else} |
87 | 212 | Citations |
88 | 213 | {/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} |
90 | 231 |
|
91 | 232 | <button |
92 | 233 | onclick={() => (showOutputDetails = { ...showOutputDetails, open: false, preview: null })} |
|
97 | 238 | </button> |
98 | 239 | </div> |
99 | 240 |
|
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'} |
118 | 242 | <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} |
123 | 259 |
|
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'} |
125 | 309 | {@const rawHtml = converter |
126 | 310 | .makeHtml(showOutputDetails.message?.content ?? '') |
127 | 311 | .replaceAll(chatCitationPattern, (match, word) => |
|
134 | 318 | ) |
135 | 319 | )} |
136 | 320 |
|
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"> |
138 | 322 | <p class="response-message flex max-w-full flex-col gap-4 whitespace-pre-line text-sm"> |
139 | 323 | {@html rawHtml} |
140 | 324 | </p> |
141 | 325 | </div> |
142 | 326 | {:else if showOutputDetails.activeTab === 'thinking'} |
143 | 327 | {@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"> |
145 | 329 | {#if showOutputDetails.reasoningTime} |
146 | 330 | <div class="mb-2 flex select-none items-center gap-2 self-start text-sm text-[#667085]"> |
147 | 331 | <Sparkle size={16} /> |
|
0 commit comments