Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tweak(ui): chat ux in list debug #82

Merged
merged 3 commits into from
Apr 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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