Skip to content

Commit 0c5f0eb

Browse files
committed
[UI] Image output columns + token usage tabs (#874)
1 parent 3168d47 commit 0c5f0eb

44 files changed

Lines changed: 1305 additions & 412 deletions

Some content is hidden

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

plan/frontend_image_output_and_metrics.md

Lines changed: 425 additions & 0 deletions
Large diffs are not rendered by default.

services/app/src/hooks.server.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,10 +123,13 @@ export const mainHandle: Handle = async ({ event, resolve }) => {
123123
}
124124
}
125125

126+
const session = !auth0Mode && !ossMode ? await event.locals.auth?.() : null;
127+
const sessionUserId = session?.user?.id;
128+
126129
//@ts-expect-error asd
127-
if (auth0UserData || ossMode) {
130+
if (auth0UserData || ossMode || sessionUserId) {
128131
//@ts-expect-error asd
129-
let userApiData = await getUserApiData(auth0UserData?.sub ?? '0');
132+
let userApiData = await getUserApiData(auth0UserData?.sub ?? sessionUserId ?? '0');
130133
if (!userApiData.data) {
131134
if (auth0Mode && userApiData.status === 404) {
132135
const userUpsertRes = await fetch(`${OWL_URL}/api/v2/users`, {

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

Lines changed: 118 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,12 @@
146146
: tableState.streamingRows[row.ID]?.includes(col.id) && !value
147147
? 'thinking'
148148
: 'answer',
149-
message: { content: value, chunks, fileUrl: tableState.rowThumbs[row.ID]?.[col.id]?.url },
149+
message: {
150+
content: value,
151+
error: cell.error ?? null,
152+
chunks,
153+
fileUrl: tableState.rowThumbs[row.ID]?.[col.id]?.url
154+
},
150155
reasoningContent: cell.reasoning_content ?? null,
151156
reasoningTime: cell.reasoning_time ?? null,
152157
expandChunk: null,
@@ -238,113 +243,131 @@
238243
</button>
239244
</div>
240245

241-
{#if showOutputDetails.activeTab !== 'image'}
242-
<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}
259-
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-
246+
{#if showOutputDetails.message?.error}
247+
<div class="flex h-1 grow flex-col gap-2 overflow-auto px-8 py-4">
271248
<p
272-
title={showOutputDetails.message?.content.split('/').pop()}
273-
class="break-all rounded text-sm text-[#667085]"
249+
class="response-message flex max-w-full flex-col gap-4 whitespace-pre-line text-sm text-[#D92D20]"
274250
>
275-
{showOutputDetails.message?.content.split('/').pop()}
251+
{typeof showOutputDetails.message.error === 'string'
252+
? showOutputDetails.message.error
253+
: showOutputDetails.message.error?.message
254+
? String(showOutputDetails.message.error.message)
255+
: 'Error'}
276256
</p>
257+
</div>
258+
{:else}
259+
{#if showOutputDetails.activeTab !== 'image'}
260+
<div
261+
data-testid="output-details-tabs"
262+
style="grid-template-columns: repeat({tabItems.length}, minmax(6rem, 1fr));"
263+
class="relative grid w-fit items-end overflow-auto border-b border-[#F2F4F7] text-xs sm:text-sm"
264+
>
265+
{#each tabItems as { id, title, condition }}
266+
{#if condition}
267+
<button
268+
onclick={() => (showOutputDetails.activeTab = id)}
269+
class="px-0 py-2 font-medium sm:px-4 {showOutputDetails.activeTab === id
270+
? 'text-[#344054]'
271+
: 'text-[#98A2B3]'} text-center transition-colors"
272+
>
273+
{title}
274+
</button>
275+
{/if}
276+
{/each}
277+
278+
<div
279+
style="width: {(1 / tabItems.length) * 100}%; left: {tabHighlightPos}%;"
280+
class="absolute bottom-0 h-[3px] bg-secondary transition-[left]"
281+
></div>
282+
</div>
283+
{/if}
277284

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]"
285+
{#if showOutputDetails.activeTab === 'image'}
286+
<div class="flex h-1 grow flex-col gap-2 p-3">
287+
<img
288+
src={showOutputDetails.message?.fileUrl}
289+
alt=""
290+
class="max-h-[45vh] object-contain"
291+
/>
292+
293+
<p
294+
title={showOutputDetails.message?.content.split('/').pop()}
295+
class="break-all rounded text-sm text-[#667085]"
284296
>
285-
<ArrowDownToLine class="h-3.5 w-3.5" />
286-
Download
287-
</Button>
297+
{showOutputDetails.message?.content.split('/').pop()}
298+
</p>
288299

289-
{#if showOutputDetails.activeCell?.rowID}
300+
<div class="flex gap-1">
290301
<Button
291302
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]"
303+
title="Download file"
304+
onclick={() => getRawFile(showOutputDetails.message?.content ?? '')}
305+
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]"
301306
>
302-
<Trash2 class="h-3.5 w-3.5" />
303-
Delete
307+
<ArrowDownToLine class="h-3.5 w-3.5" />
308+
Download
304309
</Button>
305-
{/if}
306-
</div>
307-
</div>
308-
{:else if showOutputDetails.activeTab === 'answer'}
309-
{@const rawHtml = converter
310-
.makeHtml(showOutputDetails.message?.content ?? '')
311-
.replaceAll(chatCitationPattern, (match, word) =>
312-
citationReplacer(
313-
match,
314-
word,
315-
showOutputDetails.activeCell?.columnID ?? '',
316-
showOutputDetails.activeCell?.rowID ?? '',
317-
showOutputDetails.message?.chunks ?? []
318-
)
319-
)}
320310

321-
<div class="flex h-1 grow flex-col gap-2 overflow-auto px-8 py-4">
322-
<p class="response-message flex max-w-full flex-col gap-4 whitespace-pre-line text-sm">
323-
{@html rawHtml}
324-
</p>
325-
</div>
326-
{:else if showOutputDetails.activeTab === 'thinking'}
327-
{@const rawHtml = converter.makeHtml(showOutputDetails.reasoningContent ?? '')}
328-
<div class="flex h-1 grow flex-col gap-2 overflow-auto px-8 py-4">
329-
{#if showOutputDetails.reasoningTime}
330-
<div class="mb-2 flex select-none items-center gap-2 self-start text-sm text-[#667085]">
331-
<Sparkle size={16} />
332-
Thought for {showOutputDetails.reasoningTime.toFixed()} second{Number(
333-
showOutputDetails.reasoningTime.toFixed()
334-
) > 1
335-
? 's'
336-
: ''}
311+
{#if showOutputDetails.activeCell?.rowID}
312+
<Button
313+
variant="ghost"
314+
title="Delete file"
315+
onclick={() => {
316+
tableState.deletingFile = {
317+
rowID: showOutputDetails.activeCell?.rowID ?? '',
318+
columnID: showOutputDetails.activeCell?.columnID ?? '',
319+
fileUri: showOutputDetails.message?.content ?? ''
320+
};
321+
}}
322+
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]"
323+
>
324+
<Trash2 class="h-3.5 w-3.5" />
325+
Delete
326+
</Button>
327+
{/if}
337328
</div>
338-
{/if}
329+
</div>
330+
{:else if showOutputDetails.activeTab === 'answer'}
331+
{@const rawHtml = converter
332+
.makeHtml(showOutputDetails.message?.content ?? '')
333+
.replaceAll(chatCitationPattern, (match, word) =>
334+
citationReplacer(
335+
match,
336+
word,
337+
showOutputDetails.activeCell?.columnID ?? '',
338+
showOutputDetails.activeCell?.rowID ?? '',
339+
showOutputDetails.message?.chunks ?? []
340+
)
341+
)}
342+
343+
<div class="flex h-1 grow flex-col gap-2 overflow-auto px-8 py-4">
344+
<p class="response-message flex max-w-full flex-col gap-4 whitespace-pre-line text-sm">
345+
{@html rawHtml}
346+
</p>
347+
</div>
348+
{:else if showOutputDetails.activeTab === 'thinking'}
349+
{@const rawHtml = converter.makeHtml(showOutputDetails.reasoningContent ?? '')}
350+
<div class="flex h-1 grow flex-col gap-2 overflow-auto px-8 py-4">
351+
{#if showOutputDetails.reasoningTime}
352+
<div class="mb-2 flex select-none items-center gap-2 self-start text-sm text-[#667085]">
353+
<Sparkle size={16} />
354+
Thought for {showOutputDetails.reasoningTime.toFixed()} second{Number(
355+
showOutputDetails.reasoningTime.toFixed()
356+
) > 1
357+
? 's'
358+
: ''}
359+
</div>
360+
{/if}
339361

340-
<p
341-
class="response-message flex max-w-full flex-col gap-4 whitespace-pre-line text-sm text-[#475467]"
342-
>
343-
{@html rawHtml}
344-
</p>
345-
</div>
346-
{:else if showOutputDetails.activeTab === 'references'}
347-
<References bind:showReferences={showOutputDetails} />
362+
<p
363+
class="response-message flex max-w-full flex-col gap-4 whitespace-pre-line text-sm text-[#475467]"
364+
>
365+
{@html rawHtml}
366+
</p>
367+
</div>
368+
{:else if showOutputDetails.activeTab === 'references'}
369+
<References bind:showReferences={showOutputDetails} />
370+
{/if}
348371
{/if}
349372
</div>
350373
</div>

services/app/src/lib/components/preset/ModelSelect.svelte

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,14 @@
2121
selectedModel: string;
2222
allowDeselect?: boolean;
2323
selectCb?: (modelId: string) => void;
24-
capabilityFilter?: 'completion' | 'chat' | 'image' | 'embed' | 'rerank' | undefined;
24+
capabilityFilter?:
25+
| 'completion'
26+
| 'chat'
27+
| 'image'
28+
| 'image_out'
29+
| 'embed'
30+
| 'rerank'
31+
| undefined;
2532
showCapabilities?: boolean;
2633
/** Additional trigger button class */
2734
class?: string | undefined;

services/app/src/lib/components/tables/(sub)/ColumnSettings.svelte

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969
.filter((col) => col.id !== 'ID' && col.id !== 'Updated at') ?? []
7070
);
7171
72-
let selectedTab: 'prompt' | 'rag_settings' | 'how_to_use' = $state('prompt');
72+
let selectedTab: 'prompt' | 'rag_settings' | 'how_to_use' = $state('prompt');
7373
let showModelSettings = $state(true);
7474
7575
let isLoading = $state(false);
@@ -81,7 +81,13 @@
8181
8282
const tabs: { id: typeof selectedTab; title: string }[] = [];
8383
84-
if (selectedGenConfig.object !== 'gen_config.python') {
84+
if (selectedGenConfig.object === 'gen_config.python') {
85+
tabs.push({ id: 'prompt', title: 'Code' });
86+
} else if (selectedGenConfig.object === 'gen_config.image') {
87+
if (showPromptTab) {
88+
tabs.push({ id: 'prompt', title: 'Prompt' });
89+
}
90+
} else {
8591
if (showPromptTab && selectedGenConfig.object !== 'gen_config.code') {
8692
tabs.push({ id: 'prompt', title: 'Prompt' });
8793
}
@@ -90,11 +96,9 @@
9096
id: 'rag_settings',
9197
title: selectedGenConfig.object === 'gen_config.embed' ? 'Model Settings' : 'RAG'
9298
});
93-
} else {
94-
tabs.push({ id: 'prompt', title: 'Code' });
9599
}
96100
97-
if (['gen_config.llm', 'gen_config.python'].includes(selectedGenConfig.object)) {
101+
if (['gen_config.llm', 'gen_config.python', 'gen_config.image'].includes(selectedGenConfig.object)) {
98102
tabs.push({ id: 'how_to_use', title: 'How to use' });
99103
}
100104
@@ -104,13 +108,16 @@
104108
let tabHighlightIdx = $derived(tabItems.findIndex((t) => selectedTab === t.id));
105109
106110
let promptVarCounter = $derived.by(() => {
107-
const matches = [
108-
...(selectedGenConfig?.object === 'gen_config.python'
111+
const sourceText =
112+
selectedGenConfig?.object === 'gen_config.python'
109113
? selectedGenConfig.python_code
110-
: selectedGenConfig?.object === 'gen_config.llm'
114+
: selectedGenConfig?.object === 'gen_config.llm' ||
115+
selectedGenConfig?.object === 'gen_config.image'
111116
? (selectedGenConfig?.prompt ?? '')
112-
: ''
113-
).matchAll(
117+
: '';
118+
119+
const matches = [
120+
...sourceText.matchAll(
114121
selectedGenConfig?.object === 'gen_config.python'
115122
? pythonVariablePattern
116123
: promptVariablePattern
@@ -356,6 +363,15 @@
356363
class="w-64 border-transparent bg-[#F9FAFB] hover:bg-[#e1e2e6] data-dark:bg-[#42464e]"
357364
/>
358365
</div>
366+
{:else if (tableType !== 'knowledge' || showPromptTab) && selectedGenConfig.object === 'gen_config.image'}
367+
<div class="">
368+
<ModelSelect
369+
disabled={readonly}
370+
capabilityFilter="image_out"
371+
bind:selectedModel={selectedGenConfig.model!}
372+
class="w-64 border-transparent bg-[#F9FAFB] hover:bg-[#e1e2e6] data-dark:bg-[#42464e]"
373+
/>
374+
</div>
359375
{/if}
360376

361377
<!-- {#if showPromptTab}
@@ -474,7 +490,8 @@
474490
? showModelSettings
475491
? 'sm:grid-cols-[minmax(0,8fr)_minmax(300px,8fr)]'
476492
: 'sm:grid-cols-[minmax(0,8fr)_minmax(150px,1fr)]'
477-
: ''} {selectedGenConfig.object === 'gen_config.python'
493+
: ''} {selectedGenConfig.object === 'gen_config.python' ||
494+
selectedGenConfig.object === 'gen_config.image'
478495
? 'grid-rows-1'
479496
: 'grid-rows-[repeat(2,minmax(450px,1fr))]'} min-h-0 overflow-auto"
480497
>
@@ -507,19 +524,25 @@
507524
{/each}
508525
</div>
509526

510-
{#if selectedGenConfig.object === 'gen_config.llm'}
527+
{#if selectedGenConfig.object === 'gen_config.llm' || selectedGenConfig.object === 'gen_config.image'}
511528
<PromptEditor
512529
bind:this={promptEditor}
513530
bind:editorContent={
514531
() => {
515-
if (selectedGenConfig?.object === 'gen_config.llm') {
532+
if (
533+
selectedGenConfig?.object === 'gen_config.llm' ||
534+
selectedGenConfig?.object === 'gen_config.image'
535+
) {
516536
return selectedGenConfig?.prompt ?? '';
517537
} else {
518538
return '';
519539
}
520540
},
521541
(v) => {
522-
if (selectedGenConfig?.object === 'gen_config.llm')
542+
if (
543+
selectedGenConfig?.object === 'gen_config.llm' ||
544+
selectedGenConfig?.object === 'gen_config.image'
545+
)
523546
selectedGenConfig.prompt = v;
524547
}
525548
}

services/app/src/lib/components/tables/(sub)/ColumnTypeTag.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
<span class="px-1 font-medium capitalize">
2727
{#if colType === 'input'}
2828
Input
29-
{:else if genConfig?.object === 'gen_config.llm'}
29+
{:else if genConfig?.object === 'gen_config.llm' || genConfig?.object === 'gen_config.image'}
3030
LLM
3131
{:else if genConfig?.object === 'gen_config.python'}
3232
Python

0 commit comments

Comments
 (0)