Skip to content

Commit

Permalink
tweak(ui): chat ux in list debug (#82)
Browse files Browse the repository at this point in the history
* tweak(ui): chat ux in list debug

* chore: lint

* tweak: ux
  • Loading branch information
shhdgit authored Apr 29, 2024
1 parent 7e24e25 commit 29622ef
Show file tree
Hide file tree
Showing 2 changed files with 201 additions and 36 deletions.
211 changes: 187 additions & 24 deletions ui/src/modules/app_builder/Toolbar/Debug/ListInteraction.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,90 @@
import { ActionIcon, Button, Card, FocusTrap, Stack, Text, Textarea } from '@mantine/core'
import { IconCircleX } from '@tabler/icons-react'
import { ActionIcon, Box, Button, Card, FocusTrap, Group, Stack, Text, Textarea, Tooltip } from '@mantine/core'
import { IconCircleX, IconSwitchHorizontal } from '@tabler/icons-react'
import { getHotkeyHandler, useDisclosure } from '@mantine/hooks'
import { useEffect, useMemo, useState } from 'react'
import { InteractionInfo } from '@api/linguflow.schemas'
import type { InteractionProps } from '.'

export const ListIntercation: React.FC<InteractionProps<string[]>> = ({ value = [], onChange }) => {
export const ListIntercation: React.FC<InteractionProps<string[]>> = ({
value = [],
onChange,
onSubmit,
interactions
}) => {
const [showAddInput, { open, close }] = useDisclosure(false)
const handleSubmit = (e: React.FocusEvent<HTMLTextAreaElement>) => {
const handleChange = (e: React.FocusEvent<HTMLTextAreaElement>) => {
if (!e.target.value) {
return
}
onChange([...value, e.target.value])
close()
}
const [showChat, _setShowChat] = useState(false)
const handleSetShowChat = (v: React.SetStateAction<boolean>) => {
onChange([])
_setShowChat(v)
}
const handleAddChat = (t: string[]) => {
onChange([...value, ...t])
}
const handleChangeChat = (t: string) => {
onChange([...value.slice(0, value.length - 1), t])
}

return (
<Stack>
{value?.map((item, index) => (
<ListItem
key={index}
data={item}
onDelete={() => onChange(value.filter((_, _index) => index !== _index))}
onEdit={(v) => onChange([...value.slice(0, index), v, ...value.slice(index + 1)])}
/>
))}
{showAddInput && (
<FocusTrap active>
<Textarea size="xs" autosize onBlur={handleSubmit} onKeyDown={getHotkeyHandler([['Enter', handleSubmit]])} />
</FocusTrap>
)}
{!showAddInput && (
<Button variant="default" onClick={open} style={{ borderStyle: 'dashed' }}>
Add
</Button>
)}
</Stack>
<Group align="flex-start" style={{ flexWrap: 'nowrap' }}>
<Tooltip label={showChat ? 'Switch to list mode' : 'Switch to chat mode'}>
<ActionIcon
pos="sticky"
top={0}
left={0}
style={{ zIndex: 99 }}
variant="subtle"
color="gray"
aria-label="Switch"
onClick={() => handleSetShowChat((v) => !v)}
>
<IconSwitchHorizontal style={{ width: '70%', height: '70%' }} stroke={1.5} />
</ActionIcon>
</Tooltip>
<Stack style={{ flexGrow: 1 }}>
{showChat ? (
<ListChat
interactions={interactions}
value={value}
onChange={handleChangeChat}
onAdd={handleAddChat}
onSubmit={onSubmit}
/>
) : (
<>
{value?.map((item, index) => (
<ListItem
key={index}
data={item}
onDelete={() => onChange(value.filter((_, _index) => index !== _index))}
onEdit={(v) => onChange([...value.slice(0, index), v, ...value.slice(index + 1)])}
/>
))}
{showAddInput && (
<FocusTrap active>
<Textarea
size="xs"
autosize
onBlur={handleChange}
onKeyDown={getHotkeyHandler([['Enter', handleChange]])}
/>
</FocusTrap>
)}
{!showAddInput && (
<Button variant="default" onClick={open} style={{ borderStyle: 'dashed' }}>
Add
</Button>
)}
</>
)}
</Stack>
</Group>
)
}

Expand Down Expand Up @@ -72,3 +123,115 @@ const ListItem: React.FC<{ data: string; onDelete: () => void; onEdit: (v: strin
</FocusTrap>
)
}

const ListChat: React.FC<{
value: string[]
interactions?: InteractionInfo[]
onAdd: (t: string[]) => void
onChange: (t: string) => void
onSubmit: () => void
}> = ({ value, interactions = [], onChange, onAdd, onSubmit }) => {
const [chatInput, setChatInput] = useState('')
useEffect(() => {
if (!value.length) {
return
}
onAdd([interactions[interactions.length - 1].output!, ''])
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [interactions])
const chatItems = useMemo(() => {
const items = [...value]

if ((!!chatInput && chatInput === items[items.length - 1]) || !items[items.length - 1]) {
items.pop()
}
items.reverse()

return items
}, [value, chatInput])

return (
<Stack pos="relative" maw="100%">
<Box pos="sticky" top={0} left={0} style={{ zIndex: 99 }}>
<Textarea
value={chatInput}
onChange={(e: React.FocusEvent<HTMLTextAreaElement>) => {
const t = e.target.value

if (!chatInput && value[value.length - 1] !== '' && !!t) {
onAdd([t])
} else {
onChange(t)
}

setChatInput(t)
}}
onKeyDown={getHotkeyHandler([
[
'Enter',
(e: React.FocusEvent<HTMLTextAreaElement>) => {
if (!e.target.value) {
return
}
setChatInput('')
onSubmit()
}
]
])}
/>
</Box>
<Stack>
{chatItems.map((v, i) =>
(chatItems.length - i) % 2 === 0 ? <AssistantMessage key={i} msg={v} /> : <UserMessage key={i} msg={v} />
)}
</Stack>
</Stack>
)
}

const UserMessage: React.FC<{ msg: string }> = ({ msg }) => {
return (
<Card
maw="95%"
px="md"
py="xs"
fz="sm"
bg="blue.0"
c="gray.8"
shadow="none"
style={{
flexShrink: 0,
alignSelf: 'end',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word'
}}
withBorder={false}
>
{msg}
</Card>
)
}

const AssistantMessage: React.FC<{ msg: string }> = ({ msg }) => {
return (
<Card
maw="95%"
px="md"
py="xs"
fz="sm"
bg="gray.1"
c="gray.8"
shadow="none"
style={{
position: 'relative',
flexShrink: 0,
alignSelf: 'start',
overflow: 'visible',
wordBreak: 'break-word'
}}
withBorder={false}
>
{msg}
</Card>
)
}
26 changes: 14 additions & 12 deletions ui/src/modules/app_builder/Toolbar/Debug/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ActionIcon, Box, Button, Divider, FileButton, Group, Kbd, Stack, Title, Tooltip } from '@mantine/core'
import { IconPackageExport, IconPackageImport } from '@tabler/icons-react'
import { useState } from 'react'
import { useRef, useState } from 'react'
import download from 'downloadjs'
import yaml from 'js-yaml'
import { ApplicationInfo, ApplicationVersionInfo, InteractionInfo } from '@api/linguflow.schemas'
Expand All @@ -19,14 +19,15 @@ export interface InteractionProps<V = any> {
value: V
onChange: (v: V) => void
onSubmit: () => void
interactions?: InteractionInfo[]
}

const interactionComponents: {
[k: string]: { component: React.FC<InteractionProps>; defaultValue: () => any }
[k: string]: { component: React.FC<InteractionProps>; defaultValue: (v?: any) => any }
} = {
Text_Input: { component: TextIntercation, defaultValue: () => '' },
Dict_Input: { component: ObjectIntercation, defaultValue: () => ({}) },
List_Input: { component: ListIntercation, defaultValue: () => [] }
List_Input: { component: ListIntercation, defaultValue: (v) => (v as []) || [] }
}

export const INPUT_NAMES = ['Text_Input', 'Dict_Input', 'List_Input']
Expand Down Expand Up @@ -81,7 +82,7 @@ export const Debug: React.FC<{
if (!isInteractionFinished(data.interaction)) {
return
}
setValue(InteractionComponent.defaultValue())
setValue(InteractionComponent.defaultValue)
setInteractions((v) => [...v, data.interaction!])
},
onError: (error: InteractionErrResponse) => {
Expand Down Expand Up @@ -115,7 +116,7 @@ export const Debug: React.FC<{
if (!isInteractionFinished(debugRst.interaction)) {
return
}
setValue(InteractionComponent.defaultValue())
setValue(InteractionComponent.defaultValue)
setInteractions((v) => [...v, debugRst.interaction!])
} catch (error: any) {
setIsError(true)
Expand All @@ -131,27 +132,28 @@ export const Debug: React.FC<{
}
}

const btnRef = useRef(null)

return (
<Group h="100%">
<Group align="flex-start" h="100%" style={{ flexGrow: 1 }}>
<Group h="100%" style={{ flexWrap: 'nowrap' }}>
<Group align="flex-start" h="100%" style={{ flexGrow: 1, flexWrap: 'nowrap' }}>
<Title order={6}>Input</Title>
<Box h="100%" style={{ flexGrow: 1, overflowY: 'auto' }}>
<InteractionComponent.component
value={value}
onChange={setValue}
onSubmit={() => {
return
}}
onSubmit={() => (btnRef.current as any as { click: () => void }).click()}
interactions={interactions}
/>
</Box>
<Button variant="light" loading={isLoading} onClick={runInteraction}>
<Button ref={btnRef} variant="light" style={{ flexShrink: 0 }} loading={isLoading} onClick={runInteraction}>
Send
</Button>
</Group>

<Divider orientation="vertical" />

<Stack h="100%" w="400px" style={{ overflow: 'auto' }} align="flex-start">
<Stack h="100%" w="400px" style={{ overflow: 'auto', flexShrink: 0 }} align="flex-start">
<Group gap="xs">
<Title order={6}>History(0)</Title>
<FileButton
Expand Down

0 comments on commit 29622ef

Please sign in to comment.