From c17469787edd6c401a0c69aa24d20e96d047069f Mon Sep 17 00:00:00 2001 From: Sooraj Sanker Date: Mon, 7 Oct 2024 13:00:04 +0000 Subject: [PATCH 1/2] Sync packages/ui to frontend/packages/ui --- frontend/packages/ui/constants.ts | 6 +- .../AcceptableList.stories.tsx | 4 +- .../core/acceptable-list/AcceptableList.tsx | 8 ++- .../packages/ui/core/acceptable-list/types.ts | 2 +- .../ui/core/generic-error/GenericError.tsx | 12 ++++ .../ui/core/hover-wrappers/HoverWrappers.tsx | 54 +++++++++++++++ .../packages/ui/core/hover-wrappers/index.ts | 1 + frontend/packages/ui/core/index.ts | 1 + .../ui/core/page-shell/PageShell.stories.tsx | 69 +++++++------------ .../ui/core/page-shell/components/Content.tsx | 29 +++++++- frontend/packages/ui/hooks/index.ts | 1 + .../packages/ui/hooks/useHasOverflowX.tsx | 31 +++++++++ .../ui/hooks/useMRTDefaultOptions.tsx | 6 ++ frontend/packages/ui/icons/Icons.ts | 15 ++-- frontend/packages/ui/icons/types.ts | 3 + .../packages/ui/mantine/MantineProviders.tsx | 2 +- frontend/packages/ui/mantine/mantineTheme.ts | 10 +-- .../packages/ui/storybook/MockAppShell.tsx | 24 +++++++ .../ui/storybook/MockFeatureContext.tsx | 1 + frontend/packages/ui/storybook/index.ts | 1 + frontend/packages/ui/styles/main.css | 9 ++- frontend/packages/ui/utils/index.ts | 1 - frontend/packages/ui/utils/withClassName.tsx | 41 ----------- 23 files changed, 212 insertions(+), 119 deletions(-) create mode 100644 frontend/packages/ui/core/hover-wrappers/HoverWrappers.tsx create mode 100644 frontend/packages/ui/core/hover-wrappers/index.ts create mode 100644 frontend/packages/ui/hooks/useHasOverflowX.tsx create mode 100644 frontend/packages/ui/storybook/MockAppShell.tsx delete mode 100644 frontend/packages/ui/utils/withClassName.tsx diff --git a/frontend/packages/ui/constants.ts b/frontend/packages/ui/constants.ts index 18bfeec..5d7c26b 100644 --- a/frontend/packages/ui/constants.ts +++ b/frontend/packages/ui/constants.ts @@ -1,11 +1,11 @@ import { MantineRadius, MantineSpacing } from '@mantine/core'; -export const APP_SHELL_HEADER_HEIGHT = 0; +export const APP_SHELL_HEADER_HEIGHT = 54; export const PAGE_SHELL_TAB_BAR_HEIGHT = 46; -export const PROJECT_APP_SHELL_NAVBAR_WIDTH = 64; +export const PROJECT_APP_SHELL_NAVBAR_WIDTH = 60; export const HASURA_BLUE = '#3970FD'; export const APP_SHELL_ID = 'hasura-app-shell'; -export const FONT_SIZE = 14; + export const MAX_CONTENT_WIDTH = 1280; export const CONTENT_PADDING: MantineSpacing = 'md'; export const CONTENT_MARGIN: MantineSpacing = 'md'; diff --git a/frontend/packages/ui/core/acceptable-list/AcceptableList.stories.tsx b/frontend/packages/ui/core/acceptable-list/AcceptableList.stories.tsx index ca7096a..3ddc7b5 100644 --- a/frontend/packages/ui/core/acceptable-list/AcceptableList.stories.tsx +++ b/frontend/packages/ui/core/acceptable-list/AcceptableList.stories.tsx @@ -38,9 +38,7 @@ const args: StoryObj>['args'] = { decliningIds: [], // @ts-ignore getItemId: item => item.id, - acceptAllButton: { - onClick: action('acceptAll'), - }, + onAcceptAll: action('onAcceptAll'), // @ts-ignore renderItemDetail: item => { return
{item.label}
; diff --git a/frontend/packages/ui/core/acceptable-list/AcceptableList.tsx b/frontend/packages/ui/core/acceptable-list/AcceptableList.tsx index 7f5df36..2acdd86 100644 --- a/frontend/packages/ui/core/acceptable-list/AcceptableList.tsx +++ b/frontend/packages/ui/core/acceptable-list/AcceptableList.tsx @@ -19,6 +19,7 @@ export function AcceptableList({ items, onAccept, onDecline, + onAcceptAll, acceptingAll, acceptAllButton, acceptingIds, @@ -30,20 +31,21 @@ export function AcceptableList({ }: AcceptableListProps) { return ( - {acceptAllButton && ( + {onAcceptAll && ( <> diff --git a/frontend/packages/ui/core/acceptable-list/types.ts b/frontend/packages/ui/core/acceptable-list/types.ts index 36871c8..9c02f13 100644 --- a/frontend/packages/ui/core/acceptable-list/types.ts +++ b/frontend/packages/ui/core/acceptable-list/types.ts @@ -19,9 +19,9 @@ export type AcceptableListProps = { acceptingAll: boolean; // do not allow loader customization, but otherwise allow customization acceptAllButton?: Omit & { - onClick: () => void; label?: string; }; + onAcceptAll: () => void; acceptingIds: string[]; decliningIds: string[]; getItemId: (item: ItemType) => string; diff --git a/frontend/packages/ui/core/generic-error/GenericError.tsx b/frontend/packages/ui/core/generic-error/GenericError.tsx index 57d5115..5c81f90 100644 --- a/frontend/packages/ui/core/generic-error/GenericError.tsx +++ b/frontend/packages/ui/core/generic-error/GenericError.tsx @@ -8,7 +8,9 @@ import { Stack, StackProps, } from '@mantine/core'; +import { ClientError } from 'graphql-request/build/entrypoints/main'; +import { getGraphQLErrorMessage } from '@/control-plane-client'; import { Icons } from '@/ui/icons'; import { hasMessageProperty } from '@/utils/js-utils'; import hasuraErrorLogo from './confused_hasura.png'; @@ -80,3 +82,13 @@ export const GenericError = ({ ); }; + +GenericError.GraphQLError = ({ + graphQLError, +}: { + graphQLError: ClientError; +}) => { + return ( + + ); +}; diff --git a/frontend/packages/ui/core/hover-wrappers/HoverWrappers.tsx b/frontend/packages/ui/core/hover-wrappers/HoverWrappers.tsx new file mode 100644 index 0000000..5d3fa09 --- /dev/null +++ b/frontend/packages/ui/core/hover-wrappers/HoverWrappers.tsx @@ -0,0 +1,54 @@ +import { + Box, + BoxProps, + Group, + GroupProps, + Stack, + StackProps, +} from '@mantine/core'; +import { useHover } from '@mantine/hooks'; + +export function HoverBox({ + children, + ...props +}: { + children: (hovered: boolean) => React.ReactNode; +} & Omit) { + const { ref, hovered } = useHover(); + + return ( + + {children(hovered)} + + ); +} + +export function HoverStack({ + children, + ...props +}: { + children: (hovered: boolean) => React.ReactNode; +} & Omit) { + const { ref, hovered } = useHover(); + + return ( + + {children(hovered)} + + ); +} + +export function HoverGroup({ + children, + ...props +}: { + children: (hovered: boolean) => React.ReactNode; +} & Omit) { + const { ref, hovered } = useHover(); + + return ( + + {children(hovered)} + + ); +} diff --git a/frontend/packages/ui/core/hover-wrappers/index.ts b/frontend/packages/ui/core/hover-wrappers/index.ts new file mode 100644 index 0000000..45ea03c --- /dev/null +++ b/frontend/packages/ui/core/hover-wrappers/index.ts @@ -0,0 +1 @@ +export { HoverBox, HoverStack, HoverGroup } from './HoverWrappers'; diff --git a/frontend/packages/ui/core/index.ts b/frontend/packages/ui/core/index.ts index d2bf6eb..e6236c0 100644 --- a/frontend/packages/ui/core/index.ts +++ b/frontend/packages/ui/core/index.ts @@ -24,6 +24,7 @@ export * from './page-shell'; export * from './lazy-loader'; export * from './hybrid-menu'; export * from './acceptable-list'; +export * from './hover-wrappers'; export * from './withProps'; // eslint-disable-next-line @typescript-eslint/no-restricted-imports diff --git a/frontend/packages/ui/core/page-shell/PageShell.stories.tsx b/frontend/packages/ui/core/page-shell/PageShell.stories.tsx index 31d74a2..614da22 100644 --- a/frontend/packages/ui/core/page-shell/PageShell.stories.tsx +++ b/frontend/packages/ui/core/page-shell/PageShell.stories.tsx @@ -1,9 +1,9 @@ -import { ProjectNavigationDrawer } from '@console/routing/layout'; -import { ConsoleAppShell, ErrorBoundary } from '@console/ui/common'; +import { ErrorBoundary } from '@console/ui/common'; import { Container, Skeleton } from '@mantine/core'; import { Meta, StoryObj } from '@storybook/react'; import { hasMessageProperty } from '@/utils/js-utils'; +import { MockAppShell } from '../../storybook/MockAppShell'; import { GenericError } from '../generic-error'; import { PageShellTab } from './components/TabBar'; import { PageShell } from './PageShell'; @@ -38,27 +38,6 @@ const mockTabs: PageShellTab[] = [ }, ]; -function StoryLayout({ - children, - headerText, -}: { - children: React.ReactNode; - headerText?: string; -}) { - return ( - } - header={ -
- {headerText ?? - 'Navigation does not work. Disregard the sidebar data.'} -
- } - > - {children} -
- ); -} function SomeContent() { return ( @@ -74,7 +53,7 @@ function SomeContent() { export const Basic: StoryObj = { render: () => { return ( - + @@ -86,7 +65,7 @@ export const Basic: StoryObj = { - + ); }, }; @@ -94,7 +73,7 @@ export const Basic: StoryObj = { export const WithTabBar: StoryObj = { render: () => { return ( - + @@ -106,7 +85,7 @@ export const WithTabBar: StoryObj = { - + ); }, }; @@ -114,7 +93,7 @@ export const WithTabBar: StoryObj = { export const NoHeader: StoryObj = { render: () => { return ( - + @@ -125,14 +104,14 @@ export const NoHeader: StoryObj = { - + ); }, }; export const NoHeaderWithTabs: StoryObj = { render: () => { return ( - + @@ -143,14 +122,14 @@ export const NoHeaderWithTabs: StoryObj = { - + ); }, }; export const NoSidebar: StoryObj = { render: () => { return ( - + Header @@ -159,7 +138,7 @@ export const NoSidebar: StoryObj = { - + ); }, }; @@ -167,7 +146,7 @@ export const NoSidebar: StoryObj = { export const ScrollAreaProps: StoryObj = { render: () => { return ( - + @@ -181,14 +160,14 @@ export const ScrollAreaProps: StoryObj = { - + ); }, }; export const LargerHeader: StoryObj = { render: () => { return ( - + @@ -200,14 +179,14 @@ export const LargerHeader: StoryObj = { - + ); }, }; export const ScrollClamp: StoryObj = { render: () => { return ( - + @@ -224,14 +203,14 @@ export const ScrollClamp: StoryObj = { - + ); }, }; export const BackgroundColors: StoryObj = { render: () => { return ( - + @@ -243,7 +222,7 @@ export const BackgroundColors: StoryObj = { - + ); }, }; @@ -252,7 +231,7 @@ export const PageShellHierarchyError: StoryObj = { name: ' hierarchy error', render: () => { return ( - + ( = { > foo - + ); }, }; @@ -271,7 +250,7 @@ export const PageShellMainHierarchyError: StoryObj = { name: ' hierarchy error', render: () => { return ( - + ( = { > foo - + ); }, }; diff --git a/frontend/packages/ui/core/page-shell/components/Content.tsx b/frontend/packages/ui/core/page-shell/components/Content.tsx index b8da516..d9d00a1 100644 --- a/frontend/packages/ui/core/page-shell/components/Content.tsx +++ b/frontend/packages/ui/core/page-shell/components/Content.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import { + ContainerProps, Flex, ScrollArea, ScrollAreaAutosizeProps, @@ -8,6 +9,7 @@ import { import { ExtendedCustomColors } from '@/types'; import { useSchemeColors } from '@/ui/hooks'; +import { ContentContainer } from '../../content-container/ContentContainer'; import { useIsInMain, useIsInPageShell, @@ -15,14 +17,33 @@ import { usePageShellContext, } from '../hooks'; +const Wrapper = ({ + children, + contentContainer, + contentContainerProps, +}: { + children: React.ReactNode; + contentContainer?: boolean; + contentContainerProps?: ContainerProps; +}) => { + if (contentContainer) { + return ( + {children} + ); + } else return <>{children}; +}; + export const Content = ({ children, bg, scrollAreaProps, + ...contentProps }: { children: React.ReactNode | ((height: string) => React.ReactNode); bg?: StyleProp | undefined; scrollAreaProps?: ScrollAreaAutosizeProps; + contentContainer?: boolean; + contentContainerProps?: ContainerProps; }) => { useIsInMain('Content'); useIsInPageShell('Content'); @@ -43,7 +64,9 @@ export const Content = ({ mah={height} style={{ overflow: 'hidden' }} > - {typeof children === 'function' ? children(height) : children} + + {typeof children === 'function' ? children(height) : children} + ); } @@ -55,7 +78,9 @@ export const Content = ({ {...scrollAreaProps} > - {typeof children === 'function' ? children(height) : children} + + {typeof children === 'function' ? children(height) : children} + ); diff --git a/frontend/packages/ui/hooks/index.ts b/frontend/packages/ui/hooks/index.ts index aa4e011..5b1c1ad 100644 --- a/frontend/packages/ui/hooks/index.ts +++ b/frontend/packages/ui/hooks/index.ts @@ -12,3 +12,4 @@ export * from './useHoverableTextStyle'; export * from './useUIState'; export { useLocalStorage, useSessionStorage } from './useStorage'; +export * from './useHasOverflowX'; diff --git a/frontend/packages/ui/hooks/useHasOverflowX.tsx b/frontend/packages/ui/hooks/useHasOverflowX.tsx new file mode 100644 index 0000000..ac8b7b9 --- /dev/null +++ b/frontend/packages/ui/hooks/useHasOverflowX.tsx @@ -0,0 +1,31 @@ +import { useEffect, useRef, useState } from 'react'; + +export function useHasOverflowX() { + const [hasOverflow, setHasOverflow] = useState(false); + const elementRef = useRef(null); + + useEffect(() => { + const element = elementRef.current; + if (!element) return; + + const checkOverflow = () => { + const { scrollWidth, clientWidth } = element; + setHasOverflow(scrollWidth > clientWidth); + }; + + checkOverflow(); + + // Use ResizeObserver to detect changes in size of the element and its content + const resizeObserver = new ResizeObserver(() => { + checkOverflow(); + }); + + resizeObserver.observe(element); + + return () => { + resizeObserver.disconnect(); + }; + }, []); + + return { ref: elementRef, hasOverflow }; +} diff --git a/frontend/packages/ui/hooks/useMRTDefaultOptions.tsx b/frontend/packages/ui/hooks/useMRTDefaultOptions.tsx index 85a8e0c..1df3a53 100644 --- a/frontend/packages/ui/hooks/useMRTDefaultOptions.tsx +++ b/frontend/packages/ui/hooks/useMRTDefaultOptions.tsx @@ -25,6 +25,12 @@ export function useMRTDefaultOptions(props?: { const defaultOptions = React.useMemo( () => ({ + mantineHighlightProps: { + color: 'blue', + }, + mantineTopToolbarProps: { + bg: props?.grayHeader ? bg.level4 : undefined, + }, mantineBottomToolbarProps: { p: 'lg', }, diff --git a/frontend/packages/ui/icons/Icons.ts b/frontend/packages/ui/icons/Icons.ts index c0f225d..8f8e9a0 100644 --- a/frontend/packages/ui/icons/Icons.ts +++ b/frontend/packages/ui/icons/Icons.ts @@ -1,5 +1,6 @@ import { MRT_Icons } from 'mantine-react-table'; import { BiGitCompare, BiLogoPostgresql, BiRefresh } from 'react-icons/bi'; +import { BsSortDown, BsSortUp } from 'react-icons/bs'; import { CgReadme } from 'react-icons/cg'; import { CiCloudOff, CiCloudOn } from 'react-icons/ci'; import { @@ -79,6 +80,7 @@ import { PiDatabase, PiDatabaseFill, PiDoor, + PiDotsThreeDuotone, PiDotsThreeOutline, PiDotsThreeOutlineVertical, PiDotsThreeVertical, @@ -138,9 +140,8 @@ import { PiSkipForward, PiSliders, PiSlidersHorizontal, - PiSortAscending, - PiSortDescending, PiStack, + PiStar, PiStorefront, PiSun, PiTable, @@ -190,7 +191,7 @@ export const MetadataIcons = { }; export const ProjectRoles = { - ProjectOwner: PiShieldCheck, + ProjectOwner: PiStar, ProjectAdmin: PiKey, ProjectUser: PiUser, ProjectExecuteGraphQL: PiTerminal, @@ -412,7 +413,7 @@ export const MantineReactTableIcons: Partial = { IconClearAll: PiX, IconColumns: PiTextColumns, IconDeviceFloppy: PiFloppyDisk, - IconDots: PiDotsThreeOutline, + IconDots: PiDotsThreeDuotone, IconDotsVertical: PiDotsThreeVertical, IconEdit: PiPencil, IconEyeOff: PiEyeSlash, @@ -426,13 +427,13 @@ export const MantineReactTableIcons: Partial = { IconPinnedOff: PiPushPinSlash, IconSearch: PiMagnifyingGlass, IconSearchOff: PiMagnifyingGlassMinus, - IconSortAscending: PiSortAscending, - IconSortDescending: PiSortDescending, + IconSortAscending: BsSortDown, + IconSortDescending: BsSortUp, IconX: PiX, }; export const DDNPlanIcons = { - DDNFree: PiUser, + DDNFree: PiUsers, DDNBase: FaRegBuilding, DDNAdvanced: LuBuilding2, DDNPrivate: LuServer, diff --git a/frontend/packages/ui/icons/types.ts b/frontend/packages/ui/icons/types.ts index 61d98b0..476a432 100644 --- a/frontend/packages/ui/icons/types.ts +++ b/frontend/packages/ui/icons/types.ts @@ -1,3 +1,6 @@ import { IconBaseProps } from 'react-icons/lib'; +import { Icons } from './Icons'; + export type IconProps = IconBaseProps; +export type IconKey = keyof typeof Icons; diff --git a/frontend/packages/ui/mantine/MantineProviders.tsx b/frontend/packages/ui/mantine/MantineProviders.tsx index f3f8737..162acfd 100644 --- a/frontend/packages/ui/mantine/MantineProviders.tsx +++ b/frontend/packages/ui/mantine/MantineProviders.tsx @@ -11,7 +11,7 @@ export const MantineProviders = ({ children: React.ReactNode; }) => ( - + { - return 1 / (FONT_SIZE / defaultMantineFontSize); -}; - export const defaultContainerProps = { radius: DEFAULT_RADIUS, shadow: 'none', @@ -58,7 +50,7 @@ export const mantineTheme = createTheme({ fontFamily: 'Inter, ui-sans-serif, system-ui, sans-serif', primaryColor, primaryShade: 7, - scale: calculateScale(), + scale: 1.125, // our font size is 13px. so to compensate, we scale up the mantine theme slightly (default is 16px) defaultRadius: DEFAULT_RADIUS, headings: { fontWeight: '500', diff --git a/frontend/packages/ui/storybook/MockAppShell.tsx b/frontend/packages/ui/storybook/MockAppShell.tsx new file mode 100644 index 0000000..91cf921 --- /dev/null +++ b/frontend/packages/ui/storybook/MockAppShell.tsx @@ -0,0 +1,24 @@ +import { ProjectNavigationDrawer } from '@console/routing/layout'; +import { ConsoleAppShell } from '@console/ui/common'; + +export function MockAppShell({ + children, + headerText, +}: { + children: React.ReactNode; + headerText?: string; +}) { + return ( + } + header={ +
+ {headerText ?? + 'Navigation does not work. Disregard the sidebar data.'} +
+ } + > + {children} +
+ ); +} diff --git a/frontend/packages/ui/storybook/MockFeatureContext.tsx b/frontend/packages/ui/storybook/MockFeatureContext.tsx index 6e8fece..b1e7505 100644 --- a/frontend/packages/ui/storybook/MockFeatureContext.tsx +++ b/frontend/packages/ui/storybook/MockFeatureContext.tsx @@ -30,6 +30,7 @@ const mockFeatureSet: FeatureContextData = { local: false, }, }, + dataPlaneSettings: true, settings: { projectSummary: true, usage: true, diff --git a/frontend/packages/ui/storybook/index.ts b/frontend/packages/ui/storybook/index.ts index 3860eea..d83e0ab 100644 --- a/frontend/packages/ui/storybook/index.ts +++ b/frontend/packages/ui/storybook/index.ts @@ -5,3 +5,4 @@ export { MockFeatureContext } from './MockFeatureContext'; export { mockMetadata } from './mockMetadata'; export { mockProjectData } from './mockProjectData'; export { mockUser } from './mockUser'; +export { MockAppShell } from './MockAppShell'; diff --git a/frontend/packages/ui/styles/main.css b/frontend/packages/ui/styles/main.css index 8a04cce..0ccb29a 100644 --- a/frontend/packages/ui/styles/main.css +++ b/frontend/packages/ui/styles/main.css @@ -1,7 +1,10 @@ /* To remove bootstrap html font size from 10px */ :root { - font-size: 13px; + /* Changes to this need to be synced to ../constants.ts */ + --app-font-size: 13px; + + font-size: var(--app-font-size); } .hasura-graphiql-wrapper, @@ -11,12 +14,12 @@ reach-portal, .split-sash-content, .graphiql-button-group { - --font-size-body: 13px; + --font-size-body: var(--app-font-size); } .cm-editor { /* var(--font-size-body); did not work */ - font-size: 13px; + font-size: var(--app-font-size); } .tsqd-transitions-container .tsqd-open-btn-container { diff --git a/frontend/packages/ui/utils/index.ts b/frontend/packages/ui/utils/index.ts index d574dcd..04bca77 100644 --- a/frontend/packages/ui/utils/index.ts +++ b/frontend/packages/ui/utils/index.ts @@ -1,2 +1 @@ export * from './utils'; -export { withClassName } from './withClassName'; diff --git a/frontend/packages/ui/utils/withClassName.tsx b/frontend/packages/ui/utils/withClassName.tsx deleted file mode 100644 index 23010f7..0000000 --- a/frontend/packages/ui/utils/withClassName.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react'; -import clsx from 'clsx'; - -/** - * Higher-order component that adds a className to a React component and preserves component and prop types while also forwarding refs. Especially useful for applying tailwind classes to third-party library components. - * - * @template P - Type of the props of the WrappedComponent. Defaults to `unknown`. - * - * @param {React.ComponentType

}>} WrappedComponent - The React component to wrap. - * @param {string} className - The CSS class name to add to the WrappedComponent. - * - * @returns {React.ForwardRefExoticComponent & React.RefAttributes>} - A forward ref React component that has the provided className merged with the original WrappedComponent's className (if any). - * - * @example - * const StyledComponent = withClassName(OriginalComponent, 'fixed top-0 left-0 h-full w-full'); - * - */ - -export function withClassName

( - WrappedComponent: React.ComponentType

, - className: string -): React.ForwardRefExoticComponent< - React.PropsWithoutRef

& React.RefAttributes -> { - return React.forwardRef((props, ref) => { - const propsClassName = - props && - typeof props === 'object' && - 'className' in props && - typeof props.className === 'string' && - props.className; - - return ( - - ); - }); -} From f5b3a45b964433dbfc217d364071669c6df54022 Mon Sep 17 00:00:00 2001 From: Sooraj Sanker Date: Mon, 7 Oct 2024 13:00:04 +0000 Subject: [PATCH 2/2] Sync app/features/chat to frontend/src/chat --- frontend/src/chat/ChatV2.tsx | 20 +- frontend/src/chat/PachaChat.Provider.tsx | 19 + frontend/src/chat/PachaChatContext.tsx | 49 ++- .../chat/components/ActionAuthorizeCard.tsx | 42 +- frontend/src/chat/components/Artifacts.tsx | 7 +- .../src/chat/components/AssistantResponse.tsx | 6 +- frontend/src/chat/components/ChatResponse.tsx | 39 +- .../src/chat/components/PachaChatBanner.tsx | 49 +++ .../components/PachaChatHistorySidebar.tsx | 5 +- .../components/PachaConnectionIndicator.tsx | 6 +- .../src/chat/components/ToolChainMessage.tsx | 7 +- frontend/src/chat/data/Api-Types-v3.ts | 170 ++++++++ frontend/src/chat/data/WebSocketClient.ts | 74 ++++ frontend/src/chat/data/api-types-v2.ts | 75 ---- frontend/src/chat/data/client.ts | 107 ++--- frontend/src/chat/data/hooks.ts | 6 +- frontend/src/chat/types.ts | 28 +- frontend/src/chat/usePachaChatV2.tsx | 391 ++++++++++++------ frontend/src/chat/useUserConfirmation.ts | 57 --- frontend/src/chat/utils.ts | 147 +++---- 20 files changed, 812 insertions(+), 492 deletions(-) create mode 100644 frontend/src/chat/components/PachaChatBanner.tsx create mode 100644 frontend/src/chat/data/Api-Types-v3.ts create mode 100644 frontend/src/chat/data/WebSocketClient.ts delete mode 100644 frontend/src/chat/data/api-types-v2.ts delete mode 100644 frontend/src/chat/useUserConfirmation.ts diff --git a/frontend/src/chat/ChatV2.tsx b/frontend/src/chat/ChatV2.tsx index c4e7c48..3b3be3c 100644 --- a/frontend/src/chat/ChatV2.tsx +++ b/frontend/src/chat/ChatV2.tsx @@ -1,7 +1,9 @@ -import { useContext, useEffect, useMemo, useRef, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; +import { ErrorBoundary } from '@console/ui/common'; import { ActionIcon, + GenericError, Grid, Group, PageShell, @@ -17,7 +19,7 @@ import ChatResponse from './components/ChatResponse'; import ErrorIndicator from './components/ErrorIndicator'; import PachaChatHistorySidebar from './components/PachaChatHistorySidebar'; import PachaChatProvider from './PachaChat.Provider'; -import { PachaChatContext } from './PachaChatContext'; +import { usePachaChatContext } from './PachaChatContext'; import usePachaChatV2 from './usePachaChatV2'; const handleTextareaEnterKey = @@ -69,10 +71,11 @@ export const Chat = () => { const [message, setMessage] = useState(''); const { sidebarOpen } = usePageShellContext(); - const { isMinimized, setIsMinimized } = useContext(PachaChatContext); const [textareaHeight, setTextareaHeight] = useState(100); const textareaRef = useRef(null); + const { isMinimized, setIsMinimized } = usePachaChatContext(); + const updateTextareaHeight = () => { if (textareaRef.current) { setTextareaHeight(textareaRef.current.scrollHeight); @@ -138,6 +141,7 @@ export const Chat = () => { } toolCallResponses={toolCallResponses} isQuestionPending={isQuestionPending} + error={error} /> {/* Message Input: */} @@ -210,9 +214,13 @@ export const Chat = () => { export const ChatPageShell = () => { return ( - - - + } + > + + + + ); }; diff --git a/frontend/src/chat/PachaChat.Provider.tsx b/frontend/src/chat/PachaChat.Provider.tsx index c34ffef..025b111 100644 --- a/frontend/src/chat/PachaChat.Provider.tsx +++ b/frontend/src/chat/PachaChat.Provider.tsx @@ -6,7 +6,9 @@ import { DEFAULT_PACHA_ENDPOINT, PACHA_CHAT_CONFIG_LOCAL_STORAGE_KEY, } from './constants'; +import { useThreads } from './data/hooks'; import { PachaChatContext } from './PachaChatContext'; +import { Artifact, NewAiResponse } from './types'; type PachaChatConfig = { pachaEndpoint: string; @@ -57,6 +59,15 @@ const PachaChatProvider = ({ children }: { children: React.ReactNode }) => { }, [setIsMinimized_, navigate, location.pathname] ); + const [data, setRawData] = useState([]); + const [artifacts, setArtifacts] = useState([]); + + const { + data: threads = [], + isPending: isThreadsLoading, + refetch: refetchThreads, + error: threadsError, + } = useThreads(pachaEndpoint, authToken); return ( { setPachaEndpoint, authToken, setAuthToken, + data, + setRawData, + threads, + isThreadsLoading, + refetchThreads, + threadsError, + artifacts, + setArtifacts, }} > {children} diff --git a/frontend/src/chat/PachaChatContext.tsx b/frontend/src/chat/PachaChatContext.tsx index 060bc95..379a055 100644 --- a/frontend/src/chat/PachaChatContext.tsx +++ b/frontend/src/chat/PachaChatContext.tsx @@ -1,19 +1,34 @@ -import React from 'react'; +import React, { Dispatch, SetStateAction, useContext } from 'react'; -import { DEFAULT_PACHA_ENDPOINT } from './constants'; +import { Thread } from './data/api-types'; +import { Artifact, NewAiResponse } from './types'; -export const PachaChatContext = React.createContext<{ - isMinimized: boolean; - setIsMinimized: (b: boolean) => void; - pachaEndpoint: string; - setPachaEndpoint: (endpoint: string) => void; - authToken: string; - setAuthToken: (token: string) => void; -}>({ - isMinimized: false, - setIsMinimized: b => {}, - pachaEndpoint: DEFAULT_PACHA_ENDPOINT, - setPachaEndpoint: endpoint => {}, - authToken: '', - setAuthToken: token => {}, -}); +export const PachaChatContext = React.createContext< + | { + isMinimized: boolean; + setIsMinimized: (b: boolean) => void; + pachaEndpoint: string; + setPachaEndpoint: (endpoint: string) => void; + authToken: string; + setAuthToken: (token: string) => void; + data: NewAiResponse[]; + setRawData: Dispatch>; + artifacts: Artifact[]; + setArtifacts: Dispatch>; + threads: Thread[]; + isThreadsLoading: boolean; + refetchThreads: () => void; + threadsError: Error | null; + } + | undefined +>(undefined); + +// eslint-disable-next-line react-refresh/only-export-components +export const usePachaChatContext = () => { + const context = useContext(PachaChatContext); + if (!context) { + throw Error('PachaChatContext is not provided'); + } + + return context; +}; diff --git a/frontend/src/chat/components/ActionAuthorizeCard.tsx b/frontend/src/chat/components/ActionAuthorizeCard.tsx index 1794b02..fb655c4 100644 --- a/frontend/src/chat/components/ActionAuthorizeCard.tsx +++ b/frontend/src/chat/components/ActionAuthorizeCard.tsx @@ -1,18 +1,8 @@ -import { useConsoleParams } from '@/routing'; -import { - Alert, - Button, - Card, - Group, - LoadingOverlay, - Text, - Title, -} from '@/ui/core'; +import { Alert, Button, Card, Group, Text, Title } from '@/ui/core'; import { useSchemeColors } from '@/ui/hooks'; import { Icons } from '@/ui/icons'; import { CodeMirrorProvider, ReactCodeMirror } from '@/ui/lazy'; import { UserConfirmationType } from '../types'; -import useUserConfirmation from '../useUserConfirmation'; import { safeJSONParse } from '../utils'; const getAlertRendereConfig = ({ @@ -20,13 +10,11 @@ const getAlertRendereConfig = ({ isDenied, isCanceled, isTimedOut, - error, }: { isApproved: boolean; isDenied: boolean; isCanceled: boolean; isTimedOut: boolean; - error: Error | null; }) => { if (isApproved) return { @@ -60,14 +48,6 @@ const getAlertRendereConfig = ({ icon: , message: 'Action timed out. The action will not be executed.', }; - if (error) - return { - variant: 'light', - color: 'red', - title: 'Error', - icon: , - message: 'Error updating the confirmation status. Please try again.', - }; return null; }; @@ -79,9 +59,8 @@ const ActionAuthorizeCard = ({ hasNextAiMessage: boolean; }) => { const { bg } = useSchemeColors(); - const { threadId } = useConsoleParams(); - const { update, loading, error, status } = useUserConfirmation(); + // const { update, loading, error, status } = useUserConfirmation(); const isApproved = status === 'approved' || data?.status === 'APPROVED'; const isDenied = status === 'denied' || data?.status === 'DENIED'; const isCanceled = data?.status === 'CANCELED'; @@ -95,12 +74,18 @@ const ActionAuthorizeCard = ({ isDenied, isCanceled, isTimedOut, - error, }); + const update = (b: 'approve' | 'deny') => { + data.client?.sendMessage({ + type: 'user_confirmation_response', + response: b, + confirmation_request_id: data.confirmation_id, + }); + }; return ( - + {/* */} Action Required @@ -137,14 +122,11 @@ const ActionAuthorizeCard = ({ <Button disabled={!!status} variant="light" - onClick={() => update(data?.confirmation_id, threadId ?? '', false)} + onClick={() => update('deny')} > Deny </Button> - <Button - disabled={!!status} - onClick={() => update(data?.confirmation_id, threadId ?? '', true)} - > + <Button disabled={!!status} onClick={() => update('approve')}> Approve {'>'} </Button> </Group> diff --git a/frontend/src/chat/components/Artifacts.tsx b/frontend/src/chat/components/Artifacts.tsx index 580bf42..93ad353 100644 --- a/frontend/src/chat/components/Artifacts.tsx +++ b/frontend/src/chat/components/Artifacts.tsx @@ -41,9 +41,12 @@ export const Artifacts = ({ const { updateSelectedArtifacts } = useSelectedArtifacts(); useEffect(() => { - if (artifacts.length > prevArtifacts.current.length) { + if ( + artifacts.length > prevArtifacts.current.length && + artifacts?.[artifacts?.length - 1].identifier + ) { // when new artifacts are added, select the latest one - updateSelectedArtifacts([artifacts[0].identifier]); + updateSelectedArtifacts([artifacts[artifacts?.length - 1].identifier]); setIsMinimized(false); } prevArtifacts.current = artifacts; diff --git a/frontend/src/chat/components/AssistantResponse.tsx b/frontend/src/chat/components/AssistantResponse.tsx index 59fa75c..58bd6e2 100644 --- a/frontend/src/chat/components/AssistantResponse.tsx +++ b/frontend/src/chat/components/AssistantResponse.tsx @@ -1,9 +1,7 @@ -import React, { useContext } from 'react'; - import { Button, Card, Flex, Text } from '@/ui/core'; import { useSchemeColors } from '@/ui/hooks'; import { Icons } from '@/ui/icons'; -import { PachaChatContext } from '../PachaChatContext'; +import { usePachaChatContext } from '../PachaChatContext'; import { NewAiResponse, ToolCall, ToolCallResponse } from '../types'; import useSelectedArtifacts from '../useSelectedArtifacts'; import ToolChainMessage from './ToolChainMessage'; @@ -13,7 +11,7 @@ interface ArtifactTextProps { } const ArtifactText: React.FC<ArtifactTextProps> = ({ text }) => { - const { setIsMinimized } = useContext(PachaChatContext); + const { setIsMinimized } = usePachaChatContext(); const { updateSelectedArtifacts } = useSelectedArtifacts(); if (!text) return <Text>No content</Text>; diff --git a/frontend/src/chat/components/ChatResponse.tsx b/frontend/src/chat/components/ChatResponse.tsx index 3984670..6140cbb 100644 --- a/frontend/src/chat/components/ChatResponse.tsx +++ b/frontend/src/chat/components/ChatResponse.tsx @@ -10,6 +10,7 @@ import { } from '../types'; import ActionAuthorizeCard from './ActionAuthorizeCard'; import AssistantResponse from './AssistantResponse'; +import PachaChatBanner from './PachaChatBanner'; import PachaFeedback from './PachaFeedback'; export function SelfMessageBox({ data }: { data: SelfMessage }) { @@ -35,15 +36,20 @@ export function ErrorMessage({ data }: { data: ErrorResponseType }) { const { bg } = useSchemeColors(); return ( <Paper - maw={'70%'} - my={'lg'} - withBorder style={{ whiteSpace: 'pre-wrap', }} + my={'lg'} + withBorder + p={'md'} bg={bg.color('red')} > - {data?.message} + {data?.message ?? + `An unexpected error occurred. The server did not provide specific details, but you can try the following steps: + + • Check your network connection. + • Try refreshing the page or submitting the request again. + • If the problem persists, please contact support with the steps that led to this error.`} </Paper> ); } @@ -55,6 +61,7 @@ const ChatResponse = ({ isQuestionPending, mih, mah, + error, }: { data: NewAiResponse[]; toolCallResponses: ToolCallResponse[]; @@ -62,6 +69,7 @@ const ChatResponse = ({ mah: BoxProps['mah']; mih: BoxProps['mih']; isQuestionPending: boolean; + error?: Error | null; }) => { const scrollAreaRef = useRef<HTMLDivElement>(null); const bodyRef = useRef<HTMLDivElement>(null); @@ -96,27 +104,44 @@ const ChatResponse = ({ > <Stack gap={0}> {data.map((item, index) => { + const key = `${item.type}-${index}`; if (item?.type === 'ai') return ( <AssistantResponse data={item} + key={key} toolCallResponses={toolCallResponses} /> ); - if (item?.type === 'self') return <SelfMessageBox data={item} />; - if (item?.type === 'error') return <ErrorMessage data={item} />; + if (item?.type === 'self') + return <SelfMessageBox data={item} key={key} />; + if (item?.type === 'error') + return <ErrorMessage data={item} key={key} />; if (item?.type === 'user_confirmation') return ( <ActionAuthorizeCard data={item} + key={key} hasNextAiMessage={!!data?.[index + 1]} /> ); return null; })} {isQuestionPending && <Loader type="dots" />} - {!isQuestionPending && isLastMessageFromAi && <PachaFeedback data={data} />} + {!isQuestionPending && isLastMessageFromAi && ( + <PachaFeedback data={data} /> + )} + {data?.length ? null : <PachaChatBanner />} + {error ? ( + <ErrorMessage + data={{ + message: error?.message, + type: 'error', + responseMode: 'history', + }} + /> + ) : null} </Stack> </Box> </ScrollArea.Autosize> diff --git a/frontend/src/chat/components/PachaChatBanner.tsx b/frontend/src/chat/components/PachaChatBanner.tsx new file mode 100644 index 0000000..e164d9b --- /dev/null +++ b/frontend/src/chat/components/PachaChatBanner.tsx @@ -0,0 +1,49 @@ +import { useEffect, useState } from 'react'; + +import { Alert } from '@/ui/core'; +import { useLocalStorage } from '@/ui/hooks'; +import { Icons } from '@/ui/icons'; + +const PachaChatBanner = () => { + const [showBanner, setShowBanner] = useLocalStorage({ + key: 'Pacha:showPachaChatBannerCTA', + defaultValue: true, + }); + + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + if (!mounted || !showBanner) { + return null; + } + + return ( + <Alert + icon={<Icons.Info size={20} />} + title="Introducing PromptQL Playground" + my={50} + withCloseButton + onClose={() => setShowBanner(false)} + radius={'md'} + > + Connect realtime data to your AI {' '} + <a + color="blue" + style={{ + color: 'blue', + textDecoration: 'underline', + paddingLeft: 5, + }} + href="https://hasura.io/promptql" + target="_blank" + > + Learn more → + </a> + </Alert> + ); +}; + +export default PachaChatBanner; diff --git a/frontend/src/chat/components/PachaChatHistorySidebar.tsx b/frontend/src/chat/components/PachaChatHistorySidebar.tsx index 29f80a9..e94317c 100644 --- a/frontend/src/chat/components/PachaChatHistorySidebar.tsx +++ b/frontend/src/chat/components/PachaChatHistorySidebar.tsx @@ -1,4 +1,3 @@ -import { useContext } from 'react'; import { createSearchParams, useNavigate } from 'react-router-dom'; import { getRoutes } from '@/routing'; @@ -17,7 +16,7 @@ import { useSchemeColors } from '@/ui/hooks'; import { ChatIcons, Icons } from '@/ui/icons'; import { modals } from '@/ui/modals'; import { Thread } from '../data/api-types'; -import { PachaChatContext } from '../PachaChatContext'; +import { usePachaChatContext } from '../PachaChatContext'; import { HistoryGroup } from './HistoryItem'; import { PachaChatSettingsForm } from './PachaChatSettingsForm'; import PachaConnectionIndicator from './PachaConnectionIndicator'; @@ -35,7 +34,7 @@ const PachaChatHistorySidebar = ({ const { bg } = useSchemeColors(); const { pachaEndpoint, setPachaEndpoint, authToken, setAuthToken } = - useContext(PachaChatContext); + usePachaChatContext(); const handleOpenPachaSettings = () => { modals.open({ diff --git a/frontend/src/chat/components/PachaConnectionIndicator.tsx b/frontend/src/chat/components/PachaConnectionIndicator.tsx index 969751b..bac99ba 100644 --- a/frontend/src/chat/components/PachaConnectionIndicator.tsx +++ b/frontend/src/chat/components/PachaConnectionIndicator.tsx @@ -1,13 +1,13 @@ -import React, { useContext, useEffect, useRef } from 'react'; +import { useEffect, useRef } from 'react'; import { Loader, ThemeIcon, Tooltip } from '@/ui/core'; import { Icons } from '@/ui/icons'; import { notifications } from '@/ui/notifications'; import { usePachaConnectionStatus } from '../data/hooks'; -import { PachaChatContext } from '../PachaChatContext'; +import { usePachaChatContext } from '../PachaChatContext'; const PachaConnectionIndicator = ({ onClick }: { onClick?: () => void }) => { - const { pachaEndpoint, authToken } = useContext(PachaChatContext); + const { pachaEndpoint, authToken } = usePachaChatContext(); const { data, isError, isPending } = usePachaConnectionStatus( pachaEndpoint, authToken diff --git a/frontend/src/chat/components/ToolChainMessage.tsx b/frontend/src/chat/components/ToolChainMessage.tsx index bec8edd..bc17ea2 100644 --- a/frontend/src/chat/components/ToolChainMessage.tsx +++ b/frontend/src/chat/components/ToolChainMessage.tsx @@ -14,7 +14,7 @@ import { Icons } from '@/ui/icons'; import { CodeHighlight, CodeMirrorProvider, ReactCodeMirror } from '@/ui/lazy'; import { ToolCall, ToolCallResponse } from '../types'; -const RetryingToolCall = ({ hasError }: { hasError: boolean }) => { +const ToolcallErroredOut = ({ hasError }: { hasError: boolean }) => { if (!hasError) return null; return ( <Group style={{ color: 'var(--mantine-color-yellow-filled)' }}> @@ -24,7 +24,7 @@ const RetryingToolCall = ({ hasError }: { hasError: boolean }) => { > <Icons.Warning /> </ThemeIcon> - There was an error in the output. Initiating retry... + There was an error in the output. Assessing next steps... </Group> ); }; @@ -58,7 +58,6 @@ export const ToolChainMessage = ({ const isOutputUndefined = response?.output === undefined; - if (data?.name !== 'execute_python') return null; let output: string | undefined; let hasError = false; @@ -163,7 +162,7 @@ export const ToolChainMessage = ({ </Accordion.Item> </Accordion> </Paper> - <RetryingToolCall hasError={hasError} /> + <ToolcallErroredOut hasError={hasError} /> </> ); }; diff --git a/frontend/src/chat/data/Api-Types-v3.ts b/frontend/src/chat/data/Api-Types-v3.ts new file mode 100644 index 0000000..19c5eb7 --- /dev/null +++ b/frontend/src/chat/data/Api-Types-v3.ts @@ -0,0 +1,170 @@ +// ISO 8601 +type Timestamp = string; + +type UserConfirmationResponseStatus = 'timeout' | 'approve' | 'deny'; + +interface UserConfirmationResponse { + timestamp: Timestamp; + status: UserConfirmationResponseStatus; +} + +interface UserConfirmation { + request_timestamp: Timestamp; + message: string; + response: UserConfirmationResponse | null; +} + +interface Code { + code_block_id: string; + code: string; + execution_start_timestamp: string | null; + execution_end_timestamp: string | null; + output: string | null; + error: string | null; + user_confirmations: UserConfirmation[]; + sql_statements: Array<{ sql: string; result: unknown }>; + internal_tool_call?: { + name: string; + call_id: string; + input: Record<string, string>; + }; +} + +interface AssistantAction { + action_id: string; + message?: string; + code?: Code; + error?: string; + action_end_timestamp: string; + response_start_timestamp: string; + tokens_used: number; +} + +export interface UserMessage { + type: 'user_message'; + timestamp: Timestamp; + message: string; +} + +export interface ThreadInteraction { + user_message: UserMessage; + assistant_actions: AssistantAction[]; + complete: boolean; // If this is False, it means UX should render something like "Assistant interrupted" + error?: string; +} + +interface Artifact { + artifact_type: 'table' | 'text'; + data: Record<string, unknown>[]; + identifier: string; + title: string; +} + +export interface ThreadState { + artifacts: Artifact[]; + interactions: ThreadInteraction[]; + version: 'v1'; +} + +export interface ThreadResponse { + state: ThreadState; + thread_id: string; + title: string; +} + +// Client sent events +interface ClientInit { + type: 'client_init'; + version: 'v1'; +} + +export interface UserConfirmationResponseEvent { + type: 'user_confirmation_response'; + response: 'approve' | 'deny'; + confirmation_request_id: string; +} + +export type ClientEvent = + | ClientInit + | UserMessage + | UserConfirmationResponseEvent; + +// Server events +interface CallingLlmEvent { + type: 'llm_call'; +} + +interface AcceptInteraction { + type: 'accept_interaction'; + interaction_id: string; + thread_id: string; +} + +export interface AssistantMessageResponse { + type: 'assistant_message_response'; + assistant_action_id: string; + message_chunk?: string; +} +export interface AssistantCodeResponse { + type: 'assistant_code_response'; + assistant_action_id: string; + code_block_id: string; + code_chunk?: string; +} + +interface ExecutingCode { + type: 'executing_code'; +} + +export interface CodeOutput { + type: 'code_output'; + output_chunk: string; + code_block_id: string; +} + +export interface ArtifactUpdate { + type: 'artifact_update'; + artifact: Artifact; +} + +interface CodeError { + type: 'code_error'; + code_block_id: string; + error: string; +} + +export interface UserConfirmationRequest { + type: 'user_confirmation_request'; + confirmation_request_id: string; + message: string; +} + +interface UserConfirmationTimeout { + type: 'user_confirmation_timeout'; +} + +interface ServerError { + type: 'server_error'; + message: string; +} + +interface Completion { + type: 'completion'; +} + +export type ServerEvent = + | CallingLlmEvent + | AcceptInteraction + | AssistantMessageResponse + | AssistantCodeResponse + | ExecutingCode + | CodeOutput + | ArtifactUpdate + | CodeError + | UserConfirmationRequest + | UserConfirmationTimeout + | ServerError + | Completion; + +// Combined type for all possible events +export type WebSocketEvent = ClientInit | ClientEvent | ServerEvent; diff --git a/frontend/src/chat/data/WebSocketClient.ts b/frontend/src/chat/data/WebSocketClient.ts new file mode 100644 index 0000000..f5688f8 --- /dev/null +++ b/frontend/src/chat/data/WebSocketClient.ts @@ -0,0 +1,74 @@ +import { ClientEvent, ServerEvent } from './Api-Types-v3'; + +export class WebSocketClient { + private socket: WebSocket | null = null; + private url: string; + + constructor(url: string) { + this.url = url; + } + + async connect(): Promise<void> { + return new Promise((resolve, reject) => { + this.socket = new WebSocket(this.url); + + this.socket.onopen = () => { + resolve(); + }; + this.socket.onerror = error => { + console.error('WebSocket error:', error); + reject(error); + }; + }); + } + + async disconnect(): Promise<void> { + return new Promise(resolve => { + if (this.socket) { + this.socket.onclose = () => { + this.socket = null; + resolve(); + }; + this.socket.close(); + } else { + resolve(); + } + }); + } + + async sendMessage(message: ClientEvent): Promise<void> { + return new Promise((resolve, reject) => { + if (this.socket && this.socket.readyState === WebSocket.OPEN) { + this.socket.send(JSON.stringify(message)); + resolve(); + } else { + reject(new Error('WebSocket is not connected')); + } + }); + } + async isConnected(): Promise<boolean> { + return new Promise(resolve => { + if (this.socket) { + resolve(this.socket.readyState === WebSocket.OPEN); + } else { + resolve(false); + } + }); + } + + onMessage(callback: (message: ServerEvent) => void): void { + if (this.socket) { + this.socket.onmessage = event => { + const message: ServerEvent = JSON.parse(event.data); + callback(message); + }; + } + } + onClose(callback: () => void): void { + if (this.socket) { + this.socket.onclose = () => { + callback(); + }; + } + } +} diff --git a/frontend/src/chat/data/api-types-v2.ts b/frontend/src/chat/data/api-types-v2.ts deleted file mode 100644 index 27d1026..0000000 --- a/frontend/src/chat/data/api-types-v2.ts +++ /dev/null @@ -1,75 +0,0 @@ -// /thread/<id> API V2 response types -export interface Thread { - thread_id: string; - history: HistoryItem[]; - artifacts: Artifact[]; - user_confirmations: UserConfirmation[]; -} - -export interface UserConfirmation { - confirmation_id: string; - status: 'PENDING' | 'APPROVED' | 'DENIED' | 'TIMED_OUT' | 'CANCELED'; -} - -// Types for history items -export type HistoryItem = - | UserMessage - | AssistantMessage - | ToolResponse - | UserConfirmationRequest; - -export interface UserMessage { - text: string; - type: 'user'; -} - -export interface AssistantMessage { - text: string; - tool_calls: ToolCall[]; - type: 'assistant'; -} - -export interface ToolResponse { - tool_responses: ToolResponseItem[]; - type: 'tool_response'; -} - -// Types for tool calls and responses -export interface ToolCall { - name: string; - call_id: string; - input: { - python_code: string; - }; -} - -export interface ToolResponseItem { - call_id: string; - output: ToolResponseOutput; -} - -export interface ToolResponseOutput { - output: string; - error: string | null; - sql_statements: Array<{ sql: string; result: unknown }>; - modified_artifacts: Artifact[]; -} - -// Types for artifacts -export interface Artifact { - identifier: string; - title: string; - artifact_type: string; - data: ArtifactData[]; -} - -export interface ArtifactData { - Fruit: string; - Color: string; -} - -export interface UserConfirmationRequest { - type: 'user_confirmation_request'; - message: string; - confirmation_id: string; -} diff --git a/frontend/src/chat/data/client.ts b/frontend/src/chat/data/client.ts index f39f8bd..542af27 100644 --- a/frontend/src/chat/data/client.ts +++ b/frontend/src/chat/data/client.ts @@ -6,7 +6,8 @@ import { SendMessageRequest, StartThreadResponse, } from './api-types'; -import { Thread } from './api-types-v2'; +import { ServerEvent, ThreadResponse } from './Api-Types-v3'; +import { WebSocketClient } from './WebSocketClient'; export class ChatClient { private headers: HeadersInit; @@ -30,77 +31,77 @@ export class ChatClient { return this.baseUrl ? `${this.baseUrl}${path}` : path; } - createChatStreamReader = ({ + private getThreadsUrl(thread_id: string): string { + if (thread_id && thread_id !== '') { + return this.getUrl(`/threads/${thread_id}/continue`); + } else { + return this.getUrl(`/threads/start`); + } + } + + createChatStreamReaderV2 = async ({ threadId, message, - onData, + onAssistantResponse, onThreadIdChange, onError, onComplete, }: { threadId: string; message: string; - onData: (eventName: string, dataLine: string) => void; + onAssistantResponse: (event: ServerEvent, client: WebSocketClient) => void; onThreadIdChange: (newThreadId: string) => void; onError: (error: Error) => void; onComplete: () => void; - }) => { - const reader = this.sendMessageStream({ threadId, message }).then( - response => { - if (!response.ok) { - throw new Error('Network response was not ok'); - } - return response.body!.getReader(); - } - ); + }): Promise<{ + sendMessage: (message: string) => void; + disconnect: () => void; + isConnected: () => Promise<boolean>; + }> => { + const threadsUrl = this.getThreadsUrl(threadId); - const decoder = new TextDecoder(); + const client = new WebSocketClient(threadsUrl); + await client.connect(); - // Buffering logic to handle scenarios where a chunk contains multiple events and partial events. - let buffer = ''; - const processBuffer = () => { - // separates the buffer into potential events. - const events = buffer.split('\n\n'); - - // takes all but the last element, ensuring we only process complete events. - const completeEvents = events.slice(0, -1); + client.onMessage(message => { + if (message.type === 'completion') { + client.disconnect(); + return onComplete(); + } + if (message.type === 'server_error') { + onAssistantResponse(message, client); + return onError(new Error(message.message)); + } + if (message.type === 'accept_interaction') { + return onThreadIdChange(message.thread_id); + } - //keeps the last (potentially partial) event in the buffer for the next processing cycle. - buffer = events[events.length - 1]; + return onAssistantResponse(message, client); + }); - completeEvents.forEach(event => { - // as per the spec, each event should have two lines - const [eventLine, dataLine] = event.split('\n'); - if (eventLine && dataLine) { - const eventName = eventLine.replace('event: ', ''); - const eventData = dataLine.replace('data: ', ''); + client.onClose(() => { + onComplete(); + }); + client.sendMessage({ type: 'client_init', version: 'v1' }); - if (eventName === 'start') { - const newThreadId = JSON.parse(eventData).thread_id; - onThreadIdChange(newThreadId); - } else { - onData(eventName, eventData); - } - } + client.sendMessage({ + type: 'user_message', + message, + timestamp: new Date().toISOString(), + }); + const sendMessage = (newMessage: string) => { + client.sendMessage({ + type: 'user_message', + message: newMessage, + timestamp: new Date().toISOString(), }); }; - const read = ( - reader: ReadableStreamDefaultReader<Uint8Array> - ): Promise<void> => { - return reader.read().then(({ done, value }) => { - if (done) { - processBuffer(); // Process any remaining data in the buffer - onComplete(); - return; - } - const chunkRaw = decoder.decode(value, { stream: true }); - buffer += chunkRaw; - processBuffer(); - return read(reader); - }); + + const disconnect = () => { + client.disconnect(); }; - reader.then(read).catch(onError); + return { sendMessage, disconnect, isConnected: client.isConnected }; }; async getThreads(): Promise<GetThreadsResponse> { @@ -132,7 +133,7 @@ export class ChatClient { return response.text(); } - async getThread({ threadId }: GetThreadRequest): Promise<Thread> { + async getThread({ threadId }: GetThreadRequest): Promise<ThreadResponse> { const response = await fetch(this.getUrl(`/threads/${threadId}`), { method: 'GET', credentials: 'include', diff --git a/frontend/src/chat/data/hooks.ts b/frontend/src/chat/data/hooks.ts index 5395eee..554abff 100644 --- a/frontend/src/chat/data/hooks.ts +++ b/frontend/src/chat/data/hooks.ts @@ -1,7 +1,7 @@ -import { useContext, useMemo } from 'react'; +import { useMemo } from 'react'; import { createQuery } from '@/utils/react-query'; -import { PachaChatContext } from '../PachaChatContext'; +import { usePachaChatContext } from '../PachaChatContext'; import { ChatClient } from './client'; export const getLocalChatClient = (baseUrl: string, authToken: string) => @@ -11,7 +11,7 @@ export const getLocalChatClient = (baseUrl: string, authToken: string) => }); export const usePachaLocalChatClient = () => { - const { pachaEndpoint, authToken } = useContext(PachaChatContext); + const { pachaEndpoint, authToken } = usePachaChatContext(); return useMemo(() => { return new ChatClient({ baseUrl: pachaEndpoint, diff --git a/frontend/src/chat/types.ts b/frontend/src/chat/types.ts index f909697..d6cdf76 100644 --- a/frontend/src/chat/types.ts +++ b/frontend/src/chat/types.ts @@ -1,6 +1,8 @@ +import { WebSocketClient } from './data/WebSocketClient'; + type TableArtifact = { identifier: string; - artifact_type: string; + artifact_type: 'table'; title: string; data: Record<string, unknown>[]; responseMode: ResponseMode; @@ -19,7 +21,6 @@ export type Artifact = TableArtifact | TextArtifact; // Components UI Type export type ToolCall = { - name: string; call_id: string; input: { python_code: string; // assuming this is the only input for now @@ -31,7 +32,7 @@ export type ToolCallResponse = { output: { output: string; error: string | null; - sql_statements: Array<{ sql: string; result: unknown[] }>; + sql_statements: Array<{ sql: string; result: unknown }>; modified_artifacts: []; }; }; @@ -45,6 +46,7 @@ export type UserConfirmationType = { fromHistory?: boolean; status: 'PENDING' | 'APPROVED' | 'DENIED' | 'TIMED_OUT' | 'CANCELED'; responseMode: ResponseMode; + client: WebSocketClient; }; export type SelfMessage = { @@ -60,16 +62,20 @@ export type ErrorResponseType = { }; export type ResponseMode = 'stream' | 'history'; +export type AiMessage = { + message: unknown; // no chunks here, this will always be bufferred full output received till that time + type: 'ai'; + assistant_action_id: string; + confirmation_id?: string; + tool_calls?: ToolCall[]; + code?: string; // no chunks here, this will always be bufferred full output received till that time + threadId: string | null; + responseMode: ResponseMode; +}; + export type NewAiResponse = | SelfMessage - | { - message: unknown; - type: 'ai' | 'toolchain'; - confirmation_id?: string; - tool_calls?: ToolCall[]; - threadId: string | null; - responseMode: ResponseMode; - } + | AiMessage | ErrorResponseType | UserConfirmationType; diff --git a/frontend/src/chat/usePachaChatV2.tsx b/frontend/src/chat/usePachaChatV2.tsx index 31bdfbf..725b2f3 100644 --- a/frontend/src/chat/usePachaChatV2.tsx +++ b/frontend/src/chat/usePachaChatV2.tsx @@ -1,23 +1,61 @@ -import { - useCallback, - useContext, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useConsoleParams } from '@/routing'; -import { usePachaLocalChatClient, useThreads } from './data/hooks'; -import { PachaChatContext } from './PachaChatContext'; -import { NewAiResponse, ToolCallResponse } from './types'; -import { extractModifiedArtifacts, processMessageHistory } from './utils'; +import { ArtifactUpdate, CodeOutput, ServerEvent } from './data/Api-Types-v3'; +import { usePachaLocalChatClient } from './data/hooks'; +import { WebSocketClient } from './data/WebSocketClient'; +import { usePachaChatContext } from './PachaChatContext'; +import { + Artifact, + NewAiResponse, + ToolCall, + ToolCallResponse, + UserConfirmationType, +} from './types'; +import { processMessageHistory } from './utils'; + +const updateToolCallResponses = + (event: CodeOutput) => (prev: ToolCallResponse[]) => { + const newResponses = [...prev]; + const currentToolRespIndex = prev?.findIndex(i => { + if (i.call_id === event.code_block_id) return true; + }); + + if (currentToolRespIndex >= 0) { + // partial code output found, need to merge with the existing partial the response + newResponses[currentToolRespIndex] = { + call_id: event.code_block_id, + output: { + output: `${newResponses[currentToolRespIndex]?.output?.output ?? ''}${event.output_chunk}`, + error: null, + sql_statements: [], + modified_artifacts: [], + }, + responseMode: 'stream', + } as ToolCallResponse; + return newResponses; + } else { + // first time code output, create a new tool response entry + return [ + ...prev, + { + call_id: event.code_block_id, + output: { + output: event.output_chunk, + error: null, + sql_statements: [], + modified_artifacts: [], + }, + responseMode: 'stream', + }, + ] as ToolCallResponse[]; + } + }; const usePachaChatV2 = () => { const { threadId } = useConsoleParams(); - const [data, setRawData] = useState<NewAiResponse[]>([]); const [toolCallResponses, setToolCallResponses] = useState< ToolCallResponse[] >([]); @@ -28,14 +66,24 @@ const usePachaChatV2 = () => { const currentThreadId = useRef<string | undefined>(); const localChatClient = usePachaLocalChatClient(); - const { pachaEndpoint, authToken } = useContext(PachaChatContext); - const { - data: threads = [], - isPending: isThreadsLoading, - refetch: refetchThreads, - error: threadsError, - } = useThreads(pachaEndpoint, authToken); + threads, + isThreadsLoading, + threadsError, + refetchThreads, + data, + setRawData, + artifacts, + setArtifacts, + } = usePachaChatContext(); + + const resetState = useCallback(() => { + setRawData([]); + setToolCallResponses([]); + setArtifacts([]); + setError(null); + setLoading(false); + }, [setRawData, setToolCallResponses, setArtifacts, setError, setLoading]); useEffect(() => { // when the user navigates to a new thread, load the new thread @@ -46,26 +94,26 @@ const usePachaChatV2 = () => { if (!threadId) { // if threadId is undefined, clear the chat history // user is at the chat home page - setRawData([]); - setLoading(false); + resetState(); currentThreadId.current = undefined; return; } if (threadId) { // if threadId is defined, reset the chat history - setRawData([]); + resetState(); setLoading(true); - setError(null); currentThreadId.current = threadId; // load the chat history for the new thread localChatClient .getThread({ threadId }) .then(data => { - const { history, toolcallResponses } = processMessageHistory(data); + const { history, toolcallResponses, artifacts } = + processMessageHistory(data); setRawData(history); setToolCallResponses(toolcallResponses); + setArtifacts(artifacts); return data; }) .catch(err => { @@ -79,106 +127,147 @@ const usePachaChatV2 = () => { setLoading(false); }); } - }, [threadId, localChatClient, navigate, refetchThreads]); - - const handleServerEvents = useCallback( - (eventName: string, dataLine: string) => { - const processMessage = ( - message: string - ): { - data: NewAiResponse | null; - toolcallResponses: ToolCallResponse | null; - } => { - const nullResponse = { data: null, toolcallResponses: null }; - if (!message) return nullResponse; - - if (eventName === 'error') { - const jsonData = JSON.parse(dataLine); + }, [ + threadId, + localChatClient, + navigate, + refetchThreads, + resetState, + setRawData, + setToolCallResponses, + setArtifacts, + ]); - return { - data: { - message: jsonData.error, - type: 'error', - responseMode: 'stream', - }, - toolcallResponses: null, + const handleWsEvents = useCallback( + (event: ServerEvent, client: WebSocketClient) => { + if (event.type === 'completion') { + return; + } + + // TODO handling full ecents (ignoring chunks/data buffering for now) + if (event.type === 'assistant_message_response') { + setRawData(prevData => { + const newData = { + message: event?.message_chunk, + assistant_action_id: event.assistant_action_id, + type: 'ai', + tool_calls: [] as ToolCall[], + threadId: threadId ?? null, + responseMode: 'stream', + } as NewAiResponse; + const newMessages = [...prevData, newData]; + return newMessages; + }); + } + if (event.type === 'assistant_code_response') { + setRawData(prevData => { + const newMessages = [...prevData]; + + // find the assistant message with id + const assistantMessageIndex = prevData?.findIndex( + prev => + prev.type === 'ai' && + prev.assistant_action_id === event.assistant_action_id + ); + newMessages[assistantMessageIndex] = { + // newMessages[newMessages?.length - 1] = { + ...prevData[newMessages?.length - 1], + assistant_action_id: event.assistant_action_id, + threadId: threadId ?? null, + type: 'ai', + responseMode: 'stream', + tool_calls: [ + { + call_id: event.code_block_id, + input: { + python_code: event.code_chunk, + }, + } as ToolCall, + ], }; - } - - // ignore all other events for now - if ( - eventName !== 'assistant_response' && - eventName !== 'user_confirmation' && - eventName !== 'tool_response' - ) { - return nullResponse; - } - - if (!dataLine) { - return nullResponse; - } - - try { - const jsonData = JSON.parse(dataLine); - - if (eventName === 'assistant_response') { - return { - data: { - message: jsonData.text, - type: 'ai', - tool_calls: jsonData.tool_calls, - threadId: threadId ?? null, - responseMode: 'stream', - }, - toolcallResponses: null, - }; - } else if (eventName === 'tool_response') { - return { - data: { - message: jsonData.output, - type: 'toolchain', - threadId: threadId ?? null, - responseMode: 'stream', + return newMessages; + }); + } + if (event.type === 'user_confirmation_request') { + setRawData(prevData => { + const newUserRequest: UserConfirmationType = { + type: 'user_confirmation', + message: event.message, + confirmation_id: event.confirmation_request_id, + fromHistory: false, + status: 'PENDING', + responseMode: 'stream', + client, + }; + return [...prevData, newUserRequest]; + }); + } + if (event.type === 'code_output') { + setToolCallResponses(updateToolCallResponses(event)); + } + if (event.type === 'code_error') { + setToolCallResponses(prev => { + const newResponses = [...prev]; + const currentToolRespIndex = prev?.findIndex(i => { + if (i.call_id === event.code_block_id) return true; + }); + + if (currentToolRespIndex >= 0) { + // partial code output found, need to merge with the existing partial the response + newResponses[currentToolRespIndex] = { + call_id: event.code_block_id, + output: { + ...newResponses[currentToolRespIndex]?.output, + error: event.error, }, - toolcallResponses: jsonData, - }; - } else if (eventName === 'user_confirmation') { - return { - data: { - message: JSON.stringify(jsonData.message), - confirmation_id: jsonData.confirmation_id, - type: 'user_confirmation', + } as ToolCallResponse; + return newResponses; + } else { + // first time code output, create a new tool response entry + return [ + ...prev, + { + call_id: event.code_block_id, + output: { + output: '', + error: event.error, + sql_statements: [], + modified_artifacts: [], + }, responseMode: 'stream', - status: 'PENDING', }, - toolcallResponses: null, - }; + ] as ToolCallResponse[]; } - } catch (error) { - console.error('Error parsing JSON:', error); - return nullResponse; - } - - return nullResponse; - }; - - const { data: newData, toolcallResponses: newToolCallResponses } = - processMessage(dataLine); - - if (newData) + }); + } + if (event.type === 'artifact_update') { + setArtifacts(updateArtifacts(event)); + } + if (event.type === 'server_error') { setRawData(prevData => { - const newMessages = [...prevData, newData]; + const newMessages = [ + ...prevData, + { + message: event.message, + type: 'error', + threadId: threadId ?? null, + responseMode: 'stream', + } as NewAiResponse, + ]; return newMessages; }); - - if (newToolCallResponses) - setToolCallResponses(prev => [...prev, newToolCallResponses]); + } }, - [threadId] + [threadId, setRawData, setToolCallResponses, setArtifacts] ); const sendMessage = useCallback( - (message: string) => { + async ( + message: string + ): Promise<{ + sendMessage: (message: string) => void; + disconnect: () => void; + }> => { setLoading(true); setError(null); @@ -195,28 +284,42 @@ const usePachaChatV2 = () => { return newMessages; }); - return localChatClient.createChatStreamReader({ - threadId: threadId ?? currentThreadId.current ?? '', - message, // message to send - onData: handleServerEvents, // function to handle server events - onThreadIdChange: newThreadId => { - // to capture new thread id - currentThreadId.current = newThreadId; - navigate('../../chat/thread/' + newThreadId, { replace: true }); - refetchThreads(); - }, - onError: err => { + return await localChatClient + .createChatStreamReaderV2({ + threadId: threadId ?? currentThreadId.current ?? '', + message, // message to send + onAssistantResponse: handleWsEvents, // function to handle server events + onThreadIdChange: newThreadId => { + // to capture new thread id + currentThreadId.current = newThreadId; + navigate('../../chat/thread/' + newThreadId, { replace: true }); + refetchThreads(); + }, + onError: err => { + setError(err); + setLoading(false); + }, + onComplete: () => setLoading(false), + }) + .catch(err => { setError(err); setLoading(false); - }, - onComplete: () => setLoading(false), - }); + return { + sendMessage, + disconnect: () => {}, + }; + }); }, - [navigate, threadId, handleServerEvents, refetchThreads, localChatClient] + [ + navigate, + threadId, + handleWsEvents, + refetchThreads, + localChatClient, + setRawData, + ] ); - const artifacts = useMemo(() => extractModifiedArtifacts(data), [data]); - return { threadId, sendMessage, @@ -231,4 +334,34 @@ const usePachaChatV2 = () => { }; }; +const updateArtifacts = (event: ArtifactUpdate) => (prev: Artifact[]) => { + const newArtifacts: Artifact[] = [...prev]; + const currentArtifactIndex = prev?.findIndex(i => { + if (i.identifier === event?.artifact?.identifier) return true; + }); + + if (currentArtifactIndex >= 0) { + // partial code output found, need to merge with the existing partial the response + newArtifacts[currentArtifactIndex] = { + identifier: event?.artifact?.identifier, + artifact_type: 'table', + title: event?.artifact?.title, + data: event?.artifact?.data, + responseMode: 'stream', + } as Artifact; + return newArtifacts; + } else { + // new artifact push to artifact list + return [ + ...prev, + { + identifier: event?.artifact?.identifier, + artifact_type: 'table', + title: event?.artifact?.title, + data: event?.artifact?.data, + responseMode: 'stream', + } as Artifact, + ]; + } +}; export default usePachaChatV2; diff --git a/frontend/src/chat/useUserConfirmation.ts b/frontend/src/chat/useUserConfirmation.ts deleted file mode 100644 index 56d3f62..0000000 --- a/frontend/src/chat/useUserConfirmation.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { useState } from 'react'; - -import { usePachaLocalChatClient } from './data/hooks'; - -type Status = 'approved' | 'denied' | 'error' | undefined; - -interface UseUserConfirmationReturn { - update: ( - confirmationId: string, - threadId: string, - status: boolean - ) => Promise<void>; - loading: boolean; - error: Error | null; - status: Status; -} - -export const useUserConfirmation = (): UseUserConfirmationReturn => { - const [loading, setLoading] = useState<boolean>(false); - const [error, setError] = useState<Error | null>(null); - const [status, setStatus] = useState<Status>(); - - const localChatClient = usePachaLocalChatClient(); - - const update = async ( - confirmationId: string, - threadId: string, - confirm: boolean - ): Promise<void> => { - setLoading(true); - setError(null); - - try { - const response = await localChatClient.sendUserConfirmation({ - threadId, - confirmationId, - confirm, - }); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - setStatus(confirm ? 'approved' : 'denied'); - // Handle successful response here if needed - } catch (e) { - setError(e instanceof Error ? e : new Error('An unknown error occurred')); - setStatus('error'); - } finally { - setLoading(false); - } - }; - - return { update, loading, error, status }; -}; - -export default useUserConfirmation; diff --git a/frontend/src/chat/utils.ts b/frontend/src/chat/utils.ts index 2544257..7534240 100644 --- a/frontend/src/chat/utils.ts +++ b/frontend/src/chat/utils.ts @@ -1,101 +1,72 @@ -import { Thread } from './data/api-types-v2'; -import { - Artifact, - NewAiResponse, - ToolCallResponse, - ToolchainResult, - UserConfirmationType, -} from './types'; - -export const extractModifiedArtifacts = (data: NewAiResponse[]): Artifact[] => { - const artifactMap = new Map<string, Artifact>(); - - data.forEach(item => { - if (item.type === 'toolchain') { - try { - const parsedMessage: ToolchainResult = - typeof item.message === 'string' - ? JSON.parse(item.message) - : item.message; - - if ( - parsedMessage.modified_artifacts && - parsedMessage.modified_artifacts.length > 0 - ) { - parsedMessage.modified_artifacts.forEach(artifact => { - if (artifact.identifier) { - artifactMap.set(artifact.identifier, { - ...artifact, - responseMode: item.responseMode, - }); - } - }); - } - } catch (error) { - console.error('Error parsing toolchain message:', error); - } - } - }); +import { ThreadResponse } from './data/Api-Types-v3'; +import { AiMessage, Artifact, NewAiResponse, ToolCallResponse } from './types'; - return Array.from(artifactMap.values()).reverse(); -}; - -export const processMessageHistory = (data: Thread) => { +export const processMessageHistory = (resp: ThreadResponse) => { const history: NewAiResponse[] = []; const toolcallResponses: ToolCallResponse[] = []; - - const userConfirmationsMap = data?.user_confirmations?.reduce( - (acc, confirmation) => { - acc[confirmation.confirmation_id] = confirmation.status; - return acc; - }, - {} as Record<string, UserConfirmationType['status']> - ); - - data?.history?.forEach(item => { - if (item.type === 'user') { - // set user message - history.push({ - message: item.text, - type: 'self', - threadId: data.thread_id, + const artifacts: Artifact[] = []; + const interactions = resp.state.interactions; + resp.state.artifacts.forEach(artifact => { + if (artifact.artifact_type === 'table') { + artifacts.push({ + identifier: artifact.identifier, + artifact_type: 'table', + title: artifact.title, + data: artifact.data, responseMode: 'history', - } as NewAiResponse); - } else if (item.type === 'assistant') { - // set assistant message - history.push({ - message: item.text, + }); + } + }); + + interactions?.forEach(interaction => { + history.push({ + message: interaction?.user_message?.message, + type: 'self', + threadId: resp.thread_id, + responseMode: 'history', + } as NewAiResponse); + interaction.assistant_actions.forEach(item => { + const newVal = { + message: item.message, type: 'ai', - threadId: data.thread_id, - tool_calls: item.tool_calls, + threadId: resp.thread_id, + tool_calls: [], responseMode: 'history', - } as NewAiResponse); - } else if (item.type === 'user_confirmation_request') { + assistant_action_id: item.action_id, + } as AiMessage; + + if (item.code && item.code?.code_block_id) { + newVal.tool_calls = [ + { + call_id: item.code.code_block_id, + input: { + python_code: item.code.code, + }, + }, + ]; + toolcallResponses.push({ + call_id: item.code.code_block_id, + output: { + output: item?.code?.output ?? '', + error: item.code.error ?? null, + sql_statements: item.code.sql_statements, + modified_artifacts: [], + }, + }); + } + + history.push(newVal); + }); + if (interaction.error) { history.push({ - message: item.message, - type: 'user_confirmation', - confirmation_id: item.confirmation_id, - fromHistory: true, + message: interaction.error, + type: 'error', + threadId: resp.thread_id, responseMode: 'history', - status: userConfirmationsMap[item.confirmation_id], - } as UserConfirmationType); - } else if (item.type === 'tool_response') { - item.tool_responses.forEach(toolResponse => { - history.push({ - message: toolResponse.output, - type: 'toolchain', - threadId: data.thread_id, - responseMode: 'history', - } as NewAiResponse); - - toolcallResponses.push({ - call_id: toolResponse.call_id, - output: toolResponse.output, - } as ToolCallResponse); - }); + } as NewAiResponse); } }); - return { history, toolcallResponses }; + return { history, toolcallResponses, artifacts }; }; export function downloadObjectAsCsv(