fix(frontend): improve ai chat ui (#7648)
* update chat to brand guidelines * remove useless footer * restore conversation count * nit * nit
This commit is contained in:
@@ -16,6 +16,8 @@
|
||||
noBorder?: boolean
|
||||
extraRowClasses?: { bgSelected: string; bgHover: string; class: string }
|
||||
neverShowLoader?: boolean
|
||||
preventXOverflow?: boolean
|
||||
customRow?: import('svelte').Snippet<[any]>
|
||||
columns?: import('svelte').Snippet
|
||||
extra_row?: import('svelte').Snippet<[any]>
|
||||
children?: import('svelte').Snippet<[any]>
|
||||
@@ -36,6 +38,8 @@
|
||||
class: ''
|
||||
},
|
||||
neverShowLoader = false,
|
||||
preventXOverflow = false,
|
||||
customRow,
|
||||
columns,
|
||||
extra_row,
|
||||
children,
|
||||
@@ -167,34 +171,41 @@
|
||||
{rounded}
|
||||
{noBorder}
|
||||
{neverShowLoader}
|
||||
{preventXOverflow}
|
||||
>
|
||||
{@render columns?.()}
|
||||
|
||||
<tbody class="h-full w-full">
|
||||
<Row
|
||||
on:click={() => dispatch('select', 'extraRow')}
|
||||
class={twMerge(
|
||||
extraRowClasses.class,
|
||||
selectedItemId === 'extraRow' ? extraRowClasses.bgSelected : extraRowClasses.bgHover,
|
||||
'cursor-pointer rounded-md'
|
||||
)}
|
||||
on:hover={(e) => (hovered = e.detail ? 'extraRow' : undefined)}
|
||||
>
|
||||
{@render extra_row?.({ hover: hovered === 'extraRow' })}
|
||||
</Row>
|
||||
{#if extra_row}
|
||||
<Row
|
||||
on:click={() => dispatch('select', 'extraRow')}
|
||||
class={twMerge(
|
||||
extraRowClasses.class,
|
||||
selectedItemId === 'extraRow' ? extraRowClasses.bgSelected : extraRowClasses.bgHover,
|
||||
'cursor-pointer rounded-md'
|
||||
)}
|
||||
on:hover={(e) => (hovered = e.detail ? 'extraRow' : undefined)}
|
||||
>
|
||||
{@render extra_row?.({ hover: hovered === 'extraRow' })}
|
||||
</Row>
|
||||
{/if}
|
||||
{#each items ?? [] as item, index}
|
||||
{@const hover = item.id === hovered}
|
||||
<Row
|
||||
on:click={() => dispatch('select', item)}
|
||||
class={twMerge(
|
||||
selectedItemId === item.id ? 'bg-surface-selected' : 'hover:bg-surface-hover',
|
||||
'cursor-pointer rounded-md',
|
||||
item.isNew && index === 0 ? 'animate-slideIn' : 'group'
|
||||
)}
|
||||
on:hover={(e) => (hovered = e.detail ? item.id : undefined)}
|
||||
>
|
||||
{@render children?.({ item, hover })}
|
||||
</Row>
|
||||
{#if customRow}
|
||||
{@render customRow?.({ item, hover })}
|
||||
{:else}
|
||||
<Row
|
||||
on:click={() => dispatch('select', item)}
|
||||
class={twMerge(
|
||||
selectedItemId === item.id ? 'bg-surface-selected' : 'hover:bg-surface-hover',
|
||||
'cursor-pointer rounded-md',
|
||||
item.isNew && index === 0 ? 'animate-slideIn' : 'group'
|
||||
)}
|
||||
on:hover={(e) => (hovered = e.detail ? item.id : undefined)}
|
||||
>
|
||||
{@render children?.({ item, hover })}
|
||||
</Row>
|
||||
{/if}
|
||||
{/each}
|
||||
</tbody>
|
||||
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
count: number | undefined
|
||||
small?: boolean
|
||||
alwaysVisible?: boolean
|
||||
class?: string
|
||||
}
|
||||
|
||||
let { count, small = true, alwaysVisible = false }: Props = $props()
|
||||
let { count, small = true, alwaysVisible = false, class: classNames = '' }: Props = $props()
|
||||
</script>
|
||||
|
||||
{#if count === undefined || count > 0 || alwaysVisible}
|
||||
@@ -25,7 +26,8 @@
|
||||
|
||||
// Special case for always visible
|
||||
alwaysVisible && small ? 'h-3 w-3 text-[8px] -right-0.5 -top-0.5' : '',
|
||||
alwaysVisible && !small ? 'h-4 w-4 text-xs -right-1 -top-1' : ''
|
||||
alwaysVisible && !small ? 'h-4 w-4 text-xs -right-1 -top-1' : '',
|
||||
classNames
|
||||
)}
|
||||
>
|
||||
{#if count === undefined}
|
||||
@@ -34,4 +36,4 @@
|
||||
<p>{count}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/common'
|
||||
import {
|
||||
MessageCircle,
|
||||
Plus,
|
||||
Trash2,
|
||||
PanelLeftClose,
|
||||
PanelLeftOpen,
|
||||
Loader2
|
||||
} from 'lucide-svelte'
|
||||
import { MessageCircle, Plus, Trash2, PanelLeftClose, PanelLeftOpen } from 'lucide-svelte'
|
||||
import { type FlowConversation } from '$lib/gen'
|
||||
import CountBadge from '$lib/components/common/badge/CountBadge.svelte'
|
||||
import InfiniteList from '$lib/components/InfiniteList.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { FlowChatManager } from './FlowChatManager.svelte'
|
||||
import { fade } from 'svelte/transition'
|
||||
|
||||
interface Props {
|
||||
manager: FlowChatManager
|
||||
@@ -26,35 +20,37 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="flex flex-col h-full bg-surface border-r border-gray-200 dark:border-gray-700 transition-all duration-300 {manager.isSidebarExpanded
|
||||
class="flex flex-col h-full bg-surface border-r transition-all duration-300 {manager.isSidebarExpanded
|
||||
? 'w-60'
|
||||
: 'w-16'}"
|
||||
: 'w-[44px]'}"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="flex-shrink-0 p-2 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex-shrink-0 border-b">
|
||||
<div class="flex flex-col gap-2 p-1">
|
||||
<Button
|
||||
size="sm"
|
||||
color="light"
|
||||
startIcon={{ icon: manager.isSidebarExpanded ? PanelLeftClose : PanelLeftOpen }}
|
||||
onclick={() => (manager.isSidebarExpanded = !manager.isSidebarExpanded)}
|
||||
unifiedSize="md"
|
||||
variant="subtle"
|
||||
startIcon={{
|
||||
icon: manager.isSidebarExpanded ? PanelLeftClose : PanelLeftOpen,
|
||||
classes: 'ml-[2px]'
|
||||
}}
|
||||
onClick={() => (manager.isSidebarExpanded = !manager.isSidebarExpanded)}
|
||||
iconOnly={!manager.isSidebarExpanded}
|
||||
btnClasses={manager.isSidebarExpanded ? '!justify-start' : ''}
|
||||
label="Conversations"
|
||||
btnClasses={'justify-start transition-all duration-150'}
|
||||
title="Conversations"
|
||||
>
|
||||
Conversations
|
||||
<div transition:fade={{ duration: 100 }}> Conversations </div>
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
color="light"
|
||||
startIcon={{ icon: Plus }}
|
||||
onclick={() => manager.createConversation({ clearMessages: true })}
|
||||
unifiedSize="md"
|
||||
variant="subtle"
|
||||
startIcon={{ icon: Plus, classes: 'ml-[2px]' }}
|
||||
onClick={() => manager.createConversation({ clearMessages: true })}
|
||||
title="Start new conversation"
|
||||
iconOnly={!manager.isSidebarExpanded}
|
||||
btnClasses={manager.isSidebarExpanded ? '!justify-start' : ''}
|
||||
label="New chat"
|
||||
btnClasses={'justify-start transition-all duration-150 whitespace-nowrap'}
|
||||
>
|
||||
New chat
|
||||
<div transition:fade={{ duration: 100 }}> New chat </div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -62,71 +58,79 @@
|
||||
<!-- Conversations List -->
|
||||
{#if !manager.isSidebarExpanded}
|
||||
<!-- Collapsed state - show single chat icon with badge -->
|
||||
<div class="p-2 flex flex-col items-center mt-2">
|
||||
<button
|
||||
class="relative w-[23px] h-[23px] rounded-md center-center hover:bg-surface-hover transition-all duration-100 text-secondary hover:text-primary group"
|
||||
onclick={() => (manager.isSidebarExpanded = true)}
|
||||
<div class="p-1">
|
||||
<Button
|
||||
unifiedSize="md"
|
||||
startIcon={{ icon: MessageCircle }}
|
||||
onClick={() => (manager.isSidebarExpanded = true)}
|
||||
title="{manager.conversations.length} conversation{manager.conversations.length !== 1
|
||||
? 's'
|
||||
: ''}"
|
||||
variant="subtle"
|
||||
btnClasses="w-fit px-2 relative"
|
||||
>
|
||||
<MessageCircle size={16} />
|
||||
<CountBadge count={manager.conversations.length} small={true} alwaysVisible={true} />
|
||||
</button>
|
||||
<CountBadge
|
||||
count={manager.conversations.length}
|
||||
small
|
||||
alwaysVisible={true}
|
||||
class="right-[3px] top-[3px]"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Always mount InfiniteList, but hide it when collapsed -->
|
||||
<div class="flex-1 overflow-hidden" class:hidden={!manager.isSidebarExpanded}>
|
||||
<div
|
||||
class="flex-1 overflow-hidden transition-all duration-150 p-1"
|
||||
class:hidden={!manager.isSidebarExpanded}
|
||||
>
|
||||
<InfiniteList
|
||||
bind:this={manager.conversationListComponent}
|
||||
bind:items={manager.conversations}
|
||||
selectedItemId={manager.selectedConversationId}
|
||||
noBorder={true}
|
||||
rounded={false}
|
||||
preventXOverflow={true}
|
||||
>
|
||||
{#snippet children({ item: conversation, hover })}
|
||||
<div
|
||||
class={twMerge(
|
||||
'w-full p-1',
|
||||
manager.selectedConversationId === conversation.id
|
||||
? 'bg-blue-200/30 text-blue-500 dark:bg-blue-600/30 text-blue-400'
|
||||
: ''
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
color="transparent"
|
||||
size="xs"
|
||||
onclick={() => manager.selectConversation(conversation.id, conversation.isDraft)}
|
||||
>
|
||||
<span class="flex-1 text-left text-sm font-medium text-primary truncate">
|
||||
{getConversationTitle(conversation)}
|
||||
</span>
|
||||
<button
|
||||
class="ml-2 p-1 rounded hover:bg-red-100 dark:hover:bg-red-900/30 text-red-500 transition-all {hover ||
|
||||
manager.deletingConversationId === conversation.id
|
||||
? 'opacity-100'
|
||||
: 'opacity-0'}"
|
||||
disabled={manager.deletingConversationId === conversation.id}
|
||||
onclick={(e) => {
|
||||
e.stopPropagation()
|
||||
if (conversation.isDraft) {
|
||||
// just remove first conversation as it is the draft
|
||||
manager.conversations = [...manager.conversations.slice(1)]
|
||||
} else {
|
||||
manager.conversationListComponent?.deleteItem(conversation.id)
|
||||
}
|
||||
}}
|
||||
title="Delete conversation"
|
||||
{#snippet customRow({ item: conversation, hover })}
|
||||
{#if manager.isSidebarExpanded}
|
||||
<div class={twMerge('w-full pb-1')} transition:fade={{ duration: 100, delay: 30 }}>
|
||||
<Button
|
||||
unifiedSize="md"
|
||||
variant="subtle"
|
||||
onClick={() => manager.selectConversation(conversation.id, conversation.isDraft)}
|
||||
selected={manager.selectedConversationId === conversation.id}
|
||||
btnClasses="transition-all duration-150 group"
|
||||
>
|
||||
{#if manager.deletingConversationId === conversation.id}
|
||||
<Loader2 size={14} class="animate-spin" />
|
||||
{:else}
|
||||
<Trash2 size={14} />
|
||||
{/if}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
<span class="flex-1 text-left truncate">
|
||||
{getConversationTitle(conversation)}
|
||||
</span>
|
||||
<Button
|
||||
wrapperClasses={twMerge(
|
||||
'ml-2 transition-all duration-100 opacity-0 group-hover:opacity-100',
|
||||
manager.deletingConversationId === conversation.id ? 'opacity-100' : ' '
|
||||
)}
|
||||
disabled={manager.deletingConversationId === conversation.id}
|
||||
onClick={(e) => {
|
||||
e?.stopPropagation()
|
||||
if (conversation.isDraft) {
|
||||
// just remove first conversation as it is the draft
|
||||
manager.conversations = [...manager.conversations.slice(1)]
|
||||
} else {
|
||||
manager.conversationListComponent?.deleteItem(conversation.id)
|
||||
}
|
||||
}}
|
||||
title="Delete conversation"
|
||||
destructive
|
||||
unifiedSize="xs"
|
||||
variant="subtle"
|
||||
loading={manager.deletingConversationId === conversation.id}
|
||||
iconOnly
|
||||
startIcon={{ icon: Trash2 }}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#snippet empty()}
|
||||
@@ -136,13 +140,4 @@
|
||||
{/snippet}
|
||||
</InfiniteList>
|
||||
</div>
|
||||
|
||||
{#if manager.isSidebarExpanded}
|
||||
<!-- Footer -->
|
||||
<div class="flex-shrink-0 p-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<p class="text-xs text-primary">
|
||||
{manager.conversations.length} conversation{manager.conversations.length !== 1 ? 's' : ''}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
loading?: boolean
|
||||
loadingMore?: boolean
|
||||
containerClass?: string
|
||||
preventXOverflow?: boolean
|
||||
children?: import('svelte').Snippet
|
||||
emptyMessage?: import('svelte').Snippet
|
||||
}
|
||||
@@ -62,7 +63,8 @@
|
||||
loadingMore = false,
|
||||
containerClass = '',
|
||||
children,
|
||||
emptyMessage
|
||||
emptyMessage,
|
||||
preventXOverflow = false
|
||||
}: Props = $props()
|
||||
setContext<DatatableContext>('datatable', {
|
||||
size
|
||||
@@ -127,7 +129,11 @@
|
||||
bind:clientHeight={tableHeight}
|
||||
>
|
||||
<List justify="between" gap="none" hFull={true}>
|
||||
<div class="w-full overflow-auto h-fit" bind:this={tableContainer} onscroll={handleScroll}>
|
||||
<div
|
||||
class={twMerge('w-full overflow-auto h-fit', preventXOverflow ? 'overflow-x-hidden' : '')}
|
||||
bind:this={tableContainer}
|
||||
onscroll={handleScroll}
|
||||
>
|
||||
<table class={tableFixed ? 'table-fixed w-full' : 'min-w-full'}>
|
||||
{@render children?.()}
|
||||
</table>
|
||||
|
||||
Reference in New Issue
Block a user