Navbar AI chats followup (#18336)
Addresses review comments from [PR#18161](https://github.com/twentyhq/twenty/pull/18161)
This commit is contained in:
@@ -118,6 +118,40 @@ export type AgentChatThread = {
|
||||
updatedAt: Scalars['DateTime'];
|
||||
};
|
||||
|
||||
export type AgentChatThreadConnection = {
|
||||
__typename?: 'AgentChatThreadConnection';
|
||||
/** Array of edges. */
|
||||
edges: Array<AgentChatThreadEdge>;
|
||||
/** Paging information */
|
||||
pageInfo: PageInfo;
|
||||
};
|
||||
|
||||
export type AgentChatThreadEdge = {
|
||||
__typename?: 'AgentChatThreadEdge';
|
||||
/** Cursor for this node. */
|
||||
cursor: Scalars['ConnectionCursor'];
|
||||
/** The node containing the AgentChatThread */
|
||||
node: AgentChatThread;
|
||||
};
|
||||
|
||||
export type AgentChatThreadFilter = {
|
||||
and?: InputMaybe<Array<AgentChatThreadFilter>>;
|
||||
id?: InputMaybe<UuidFilterComparison>;
|
||||
or?: InputMaybe<Array<AgentChatThreadFilter>>;
|
||||
updatedAt?: InputMaybe<DateFieldComparison>;
|
||||
};
|
||||
|
||||
export type AgentChatThreadSort = {
|
||||
direction: SortDirection;
|
||||
field: AgentChatThreadSortFields;
|
||||
nulls?: InputMaybe<SortNulls>;
|
||||
};
|
||||
|
||||
export enum AgentChatThreadSortFields {
|
||||
id = 'id',
|
||||
updatedAt = 'updatedAt'
|
||||
}
|
||||
|
||||
export type AgentIdInput = {
|
||||
/** The id of the agent. */
|
||||
id: Scalars['UUID'];
|
||||
@@ -1320,6 +1354,26 @@ export enum DatabaseEventAction {
|
||||
UPSERTED = 'UPSERTED'
|
||||
}
|
||||
|
||||
export type DateFieldComparison = {
|
||||
between?: InputMaybe<DateFieldComparisonBetween>;
|
||||
eq?: InputMaybe<Scalars['DateTime']>;
|
||||
gt?: InputMaybe<Scalars['DateTime']>;
|
||||
gte?: InputMaybe<Scalars['DateTime']>;
|
||||
in?: InputMaybe<Array<Scalars['DateTime']>>;
|
||||
is?: InputMaybe<Scalars['Boolean']>;
|
||||
isNot?: InputMaybe<Scalars['Boolean']>;
|
||||
lt?: InputMaybe<Scalars['DateTime']>;
|
||||
lte?: InputMaybe<Scalars['DateTime']>;
|
||||
neq?: InputMaybe<Scalars['DateTime']>;
|
||||
notBetween?: InputMaybe<DateFieldComparisonBetween>;
|
||||
notIn?: InputMaybe<Array<Scalars['DateTime']>>;
|
||||
};
|
||||
|
||||
export type DateFieldComparisonBetween = {
|
||||
lower: Scalars['DateTime'];
|
||||
upper: Scalars['DateTime'];
|
||||
};
|
||||
|
||||
export type DeleteApprovedAccessDomainInput = {
|
||||
id: Scalars['UUID'];
|
||||
};
|
||||
@@ -3868,7 +3922,7 @@ export type Query = {
|
||||
billingPortalSession: BillingSession;
|
||||
chatMessages: Array<AgentMessage>;
|
||||
chatThread: AgentChatThread;
|
||||
chatThreads: Array<AgentChatThread>;
|
||||
chatThreads: AgentChatThreadConnection;
|
||||
checkUserExists: CheckUserExist;
|
||||
checkWorkspaceInviteHashIsValid: WorkspaceInviteHashValid;
|
||||
commandMenuItem?: Maybe<CommandMenuItem>;
|
||||
@@ -3987,6 +4041,13 @@ export type QueryChatThreadArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type QueryChatThreadsArgs = {
|
||||
filter?: AgentChatThreadFilter;
|
||||
paging?: CursorPaging;
|
||||
sorting?: Array<AgentChatThreadSort>;
|
||||
};
|
||||
|
||||
|
||||
export type QueryCheckUserExistsArgs = {
|
||||
captchaToken?: InputMaybe<Scalars['String']>;
|
||||
email: Scalars['String'];
|
||||
@@ -4594,6 +4655,18 @@ export type Skill = {
|
||||
updatedAt: Scalars['DateTime'];
|
||||
};
|
||||
|
||||
/** Sort Directions */
|
||||
export enum SortDirection {
|
||||
ASC = 'ASC',
|
||||
DESC = 'DESC'
|
||||
}
|
||||
|
||||
/** Sort Nulls Options */
|
||||
export enum SortNulls {
|
||||
NULLS_FIRST = 'NULLS_FIRST',
|
||||
NULLS_LAST = 'NULLS_LAST'
|
||||
}
|
||||
|
||||
export type StandaloneRichTextConfiguration = {
|
||||
__typename?: 'StandaloneRichTextConfiguration';
|
||||
body: RichTextV2Body;
|
||||
@@ -5690,10 +5763,12 @@ export type GetChatMessagesQueryVariables = Exact<{
|
||||
|
||||
export type GetChatMessagesQuery = { __typename?: 'Query', chatMessages: Array<{ __typename?: 'AgentMessage', id: string, threadId: string, turnId: string, role: string, createdAt: string, parts: Array<{ __typename?: 'AgentMessagePart', id: string, messageId: string, orderIndex: number, type: string, textContent?: string | null, reasoningContent?: string | null, toolName?: string | null, toolCallId?: string | null, toolInput?: any | null, toolOutput?: any | null, state?: string | null, errorMessage?: string | null, errorDetails?: any | null, sourceUrlSourceId?: string | null, sourceUrlUrl?: string | null, sourceUrlTitle?: string | null, sourceDocumentSourceId?: string | null, sourceDocumentMediaType?: string | null, sourceDocumentTitle?: string | null, sourceDocumentFilename?: string | null, fileMediaType?: string | null, fileFilename?: string | null, fileUrl?: string | null, providerMetadata?: any | null, createdAt: string }> }> };
|
||||
|
||||
export type GetChatThreadsQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
export type GetChatThreadsQueryVariables = Exact<{
|
||||
paging?: InputMaybe<CursorPaging>;
|
||||
}>;
|
||||
|
||||
|
||||
export type GetChatThreadsQuery = { __typename?: 'Query', chatThreads: Array<{ __typename?: 'AgentChatThread', id: string, title?: string | null, totalInputTokens: number, totalOutputTokens: number, contextWindowTokens?: number | null, conversationSize: number, totalInputCredits: number, totalOutputCredits: number, createdAt: string, updatedAt: string }> };
|
||||
export type GetChatThreadsQuery = { __typename?: 'Query', chatThreads: { __typename?: 'AgentChatThreadConnection', edges: Array<{ __typename?: 'AgentChatThreadEdge', cursor: any, node: { __typename?: 'AgentChatThread', id: string, title?: string | null, totalInputTokens: number, totalOutputTokens: number, contextWindowTokens?: number | null, conversationSize: number, totalInputCredits: number, totalOutputCredits: number, createdAt: string, updatedAt: string } }>, pageInfo: { __typename?: 'PageInfo', endCursor?: any | null, hasNextPage?: boolean | null } } };
|
||||
|
||||
export type GetToolIndexQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
@@ -9048,18 +9123,27 @@ export type GetChatMessagesQueryHookResult = ReturnType<typeof useGetChatMessage
|
||||
export type GetChatMessagesLazyQueryHookResult = ReturnType<typeof useGetChatMessagesLazyQuery>;
|
||||
export type GetChatMessagesQueryResult = Apollo.QueryResult<GetChatMessagesQuery, GetChatMessagesQueryVariables>;
|
||||
export const GetChatThreadsDocument = gql`
|
||||
query GetChatThreads {
|
||||
chatThreads {
|
||||
id
|
||||
title
|
||||
totalInputTokens
|
||||
totalOutputTokens
|
||||
contextWindowTokens
|
||||
conversationSize
|
||||
totalInputCredits
|
||||
totalOutputCredits
|
||||
createdAt
|
||||
updatedAt
|
||||
query GetChatThreads($paging: CursorPaging) {
|
||||
chatThreads(paging: $paging) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
title
|
||||
totalInputTokens
|
||||
totalOutputTokens
|
||||
contextWindowTokens
|
||||
conversationSize
|
||||
totalInputCredits
|
||||
totalOutputCredits
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
cursor
|
||||
}
|
||||
pageInfo {
|
||||
endCursor
|
||||
hasNextPage
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -9076,6 +9160,7 @@ export const GetChatThreadsDocument = gql`
|
||||
* @example
|
||||
* const { data, loading, error } = useGetChatThreadsQuery({
|
||||
* variables: {
|
||||
* paging: // value for 'paging'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
|
||||
@@ -107,7 +107,7 @@ const StyledScrollWrapper = styled(ScrollWrapper)`
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
overflow-y: auto;
|
||||
padding: ${themeCssVariables.spacing[3]};
|
||||
width: calc(100% - 24px);
|
||||
width: calc(100% - 24px) !important;
|
||||
`;
|
||||
|
||||
const StyledButtonsContainer = styled.div`
|
||||
|
||||
@@ -3,6 +3,7 @@ import { styled } from '@linaria/react';
|
||||
import { AIChatThreadGroup } from '@/ai/components/AIChatThreadGroup';
|
||||
import { AIChatThreadsListEffect } from '@/ai/components/AIChatThreadsListEffect';
|
||||
import { AIChatSkeletonLoader } from '@/ai/components/internal/AIChatSkeletonLoader';
|
||||
import { useChatThreads } from '@/ai/hooks/useChatThreads';
|
||||
import { useCreateNewAIChatThread } from '@/ai/hooks/useCreateNewAIChatThread';
|
||||
import { groupThreadsByDate } from '@/ai/utils/groupThreadsByDate';
|
||||
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
|
||||
@@ -12,7 +13,6 @@ import { capitalize } from 'twenty-shared/utils';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { getOsControlSymbol } from 'twenty-ui/utilities';
|
||||
import { useGetChatThreadsQuery } from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
background: ${themeCssVariables.background.secondary};
|
||||
@@ -47,11 +47,11 @@ export const AIChatThreadsList = () => {
|
||||
dependencies: [createChatThread],
|
||||
});
|
||||
|
||||
const { data: { chatThreads = [] } = {}, loading } = useGetChatThreadsQuery();
|
||||
const { threads, hasNextPage, loading, fetchMoreRef } = useChatThreads();
|
||||
|
||||
const groupedThreads = groupThreadsByDate(chatThreads);
|
||||
const groupedThreads = groupThreadsByDate(threads);
|
||||
|
||||
if (loading === true) {
|
||||
if (loading && threads.length === 0) {
|
||||
return <AIChatSkeletonLoader />;
|
||||
}
|
||||
|
||||
@@ -60,13 +60,16 @@ export const AIChatThreadsList = () => {
|
||||
<AIChatThreadsListEffect focusId={focusId} />
|
||||
<StyledContainer>
|
||||
<StyledThreadsContainer>
|
||||
{Object.entries(groupedThreads).map(([title, threads]) => (
|
||||
{Object.entries(groupedThreads).map(([title, threadsInGroup]) => (
|
||||
<AIChatThreadGroup
|
||||
key={title}
|
||||
title={capitalize(title)}
|
||||
threads={threads}
|
||||
threads={threadsInGroup}
|
||||
/>
|
||||
))}
|
||||
{hasNextPage ? (
|
||||
<div ref={fetchMoreRef} style={{ minHeight: 1 }} />
|
||||
) : null}
|
||||
</StyledThreadsContainer>
|
||||
<StyledButtonsContainer>
|
||||
<Button
|
||||
|
||||
+17
-9
@@ -1,16 +1,16 @@
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
import { NavigationDrawerAIChatThreadDateSection } from '@/ai/components/NavigationDrawerAIChatThreadDateSection';
|
||||
import { AIChatSkeletonLoader } from '@/ai/components/internal/AIChatSkeletonLoader';
|
||||
import { NavigationDrawerAIChatThreadDateSection } from '@/ai/components/NavigationDrawerAIChatThreadDateSection';
|
||||
import { useAIChatThreadClick } from '@/ai/hooks/useAIChatThreadClick';
|
||||
import { useChatThreads } from '@/ai/hooks/useChatThreads';
|
||||
import { currentAIChatThreadState } from '@/ai/states/currentAIChatThreadState';
|
||||
import { groupThreadsByDate } from '@/ai/utils/groupThreadsByDate';
|
||||
import { type DateGroupKey } from '@/ai/utils/dateGroupKey';
|
||||
import { DATE_GROUP_KEYS } from '@/ai/utils/dateGroupKeys';
|
||||
import { getDateGroupTitle } from '@/ai/utils/getDateGroupTitle';
|
||||
import { groupThreadsByDate } from '@/ai/utils/groupThreadsByDate';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { useGetChatThreadsQuery } from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledScrollableList = styled.div`
|
||||
display: flex;
|
||||
@@ -22,35 +22,43 @@ const StyledScrollableList = styled.div`
|
||||
width: calc(100% - ${themeCssVariables.spacing[2]});
|
||||
`;
|
||||
|
||||
const StyledFetchMoreTrigger = styled.div`
|
||||
height: 1px;
|
||||
min-height: 1px;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const NavigationDrawerAIChatThreadsList = () => {
|
||||
const currentAIChatThread = useAtomStateValue(currentAIChatThreadState);
|
||||
const { handleThreadClick } = useAIChatThreadClick({
|
||||
resetNavigationStack: true,
|
||||
});
|
||||
|
||||
const { data: { chatThreads = [] } = {}, loading } = useGetChatThreadsQuery();
|
||||
const groupedThreads = groupThreadsByDate(chatThreads);
|
||||
const { threads, hasNextPage, loading, fetchMoreRef } = useChatThreads();
|
||||
|
||||
if (loading === true) {
|
||||
const groupedThreads = groupThreadsByDate(threads);
|
||||
|
||||
if (loading && threads.length === 0) {
|
||||
return <AIChatSkeletonLoader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledScrollableList>
|
||||
{DATE_GROUP_KEYS.map((key: DateGroupKey) => {
|
||||
const threads = groupedThreads[key];
|
||||
if (threads.length === 0) return null;
|
||||
const threadsInGroup = groupedThreads[key];
|
||||
if (threadsInGroup.length === 0) return null;
|
||||
|
||||
return (
|
||||
<NavigationDrawerAIChatThreadDateSection
|
||||
key={key}
|
||||
title={getDateGroupTitle(key)}
|
||||
threads={threads}
|
||||
threads={threadsInGroup}
|
||||
currentThreadId={currentAIChatThread}
|
||||
onThreadClick={handleThreadClick}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{hasNextPage ? <StyledFetchMoreTrigger ref={fetchMoreRef} /> : null}
|
||||
</StyledScrollableList>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const CHAT_THREADS_PAGE_SIZE = 20;
|
||||
@@ -1,18 +1,27 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const GET_CHAT_THREADS = gql`
|
||||
query GetChatThreads {
|
||||
chatThreads {
|
||||
id
|
||||
title
|
||||
totalInputTokens
|
||||
totalOutputTokens
|
||||
contextWindowTokens
|
||||
conversationSize
|
||||
totalInputCredits
|
||||
totalOutputCredits
|
||||
createdAt
|
||||
updatedAt
|
||||
query GetChatThreads($paging: CursorPaging) {
|
||||
chatThreads(paging: $paging) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
title
|
||||
totalInputTokens
|
||||
totalOutputTokens
|
||||
contextWindowTokens
|
||||
conversationSize
|
||||
totalInputCredits
|
||||
totalOutputCredits
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
cursor
|
||||
}
|
||||
pageInfo {
|
||||
endCursor
|
||||
hasNextPage
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
|
||||
import { useGetBrowsingContext } from '@/ai/hooks/useBrowsingContext';
|
||||
import { agentChatSelectedFilesState } from '@/ai/states/agentChatSelectedFilesState';
|
||||
import { agentChatUploadedFilesState } from '@/ai/states/agentChatUploadedFilesState';
|
||||
@@ -6,13 +8,13 @@ import { currentAIChatThreadState } from '@/ai/states/currentAIChatThreadState';
|
||||
import { currentAIChatThreadTitleState } from '@/ai/states/currentAIChatThreadTitleState';
|
||||
|
||||
import { agentChatInputState } from '@/ai/states/agentChatInputState';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { REST_API_BASE_URL } from '@/apollo/constant/rest-api-base-url';
|
||||
import { getTokenPair } from '@/apollo/utils/getTokenPair';
|
||||
import { renewToken } from '@/auth/services/AuthService';
|
||||
import { tokenPairState } from '@/auth/states/tokenPairState';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { useChat } from '@ai-sdk/react';
|
||||
import { DefaultChatTransport } from 'ai';
|
||||
import { type ExtendedUIMessage } from 'twenty-shared/ai';
|
||||
@@ -28,6 +30,7 @@ export const useAgentChat = (uiMessages: ExtendedUIMessage[]) => {
|
||||
const setCurrentAIChatThreadTitle = useSetAtomState(
|
||||
currentAIChatThreadTitleState,
|
||||
);
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const agentChatSelectedFiles = useAtomStateValue(agentChatSelectedFilesState);
|
||||
|
||||
@@ -161,6 +164,20 @@ export const useAgentChat = (uiMessages: ExtendedUIMessage[]) => {
|
||||
|
||||
if (isDefined(titlePart) && titlePart.type === 'data-thread-title') {
|
||||
setCurrentAIChatThreadTitle(titlePart.data.title);
|
||||
if (isDefined(currentAIChatThread)) {
|
||||
const threadRef = apolloClient.cache.identify({
|
||||
__typename: 'AgentChatThread',
|
||||
id: currentAIChatThread,
|
||||
});
|
||||
if (isDefined(threadRef)) {
|
||||
apolloClient.cache.modify({
|
||||
id: threadRef,
|
||||
fields: {
|
||||
title: () => titlePart.data.title,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
import { type SetStateAction } from 'jotai';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { CHAT_THREADS_PAGE_SIZE } from '@/ai/constants/ChatThreads';
|
||||
import { useAgentChatScrollToBottom } from '@/ai/hooks/useAgentChatScrollToBottom';
|
||||
import {
|
||||
agentChatUsageState,
|
||||
@@ -9,10 +14,10 @@ import { isCreatingChatThreadState } from '@/ai/states/isCreatingChatThreadState
|
||||
import { mapDBMessagesToUIMessages } from '@/ai/utils/mapDBMessagesToUIMessages';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { type SetStateAction } from 'jotai';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
type AgentChatThread,
|
||||
GetChatThreadsDocument,
|
||||
useCreateChatThreadMutation,
|
||||
useGetChatMessagesQuery,
|
||||
useGetChatThreadsQuery,
|
||||
@@ -66,13 +71,19 @@ export const useAgentChatData = () => {
|
||||
onError: () => {
|
||||
setIsCreatingChatThread(false);
|
||||
},
|
||||
refetchQueries: [
|
||||
getOperationName(GetChatThreadsDocument) ?? 'GetChatThreads',
|
||||
],
|
||||
});
|
||||
|
||||
const { loading: threadsLoading } = useGetChatThreadsQuery({
|
||||
variables: { paging: { first: CHAT_THREADS_PAGE_SIZE } },
|
||||
skip: isDefined(currentAIChatThread),
|
||||
onCompleted: (data) => {
|
||||
if (data.chatThreads.length > 0) {
|
||||
const firstThread = data.chatThreads[0];
|
||||
const edges = data?.chatThreads?.edges ?? [];
|
||||
const threads = edges.map((edge) => edge.node);
|
||||
if (threads.length > 0) {
|
||||
const firstThread = threads[0];
|
||||
|
||||
setCurrentAIChatThread(firstThread.id);
|
||||
setCurrentAIChatThreadTitle(firstThread.title ?? null);
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { CHAT_THREADS_PAGE_SIZE } from '@/ai/constants/ChatThreads';
|
||||
|
||||
import { useGetChatThreadsQuery } from '~/generated-metadata/graphql';
|
||||
|
||||
const FETCH_MORE_ROOT_MARGIN = '200px';
|
||||
|
||||
export const useChatThreads = () => {
|
||||
const [shouldFetchMore, setShouldFetchMore] = useState(false);
|
||||
const { data, loading, fetchMore } = useGetChatThreadsQuery({
|
||||
variables: {
|
||||
paging: { first: CHAT_THREADS_PAGE_SIZE },
|
||||
},
|
||||
onCompleted: () => {
|
||||
setShouldFetchMore(false);
|
||||
},
|
||||
});
|
||||
|
||||
const edges = data?.chatThreads?.edges ?? [];
|
||||
const threads = edges.map((edge) => edge.node);
|
||||
const pageInfo = data?.chatThreads?.pageInfo;
|
||||
const endCursor = pageInfo?.endCursor ?? undefined;
|
||||
const hasNextPage = pageInfo?.hasNextPage ?? false;
|
||||
|
||||
const { ref: fetchMoreRef, inView } = useInView({
|
||||
rootMargin: FETCH_MORE_ROOT_MARGIN,
|
||||
});
|
||||
|
||||
const loadMore = useCallback(() => {
|
||||
if (!hasNextPage || loading || !endCursor) {
|
||||
return;
|
||||
}
|
||||
|
||||
return fetchMore({
|
||||
variables: {
|
||||
paging: {
|
||||
first: CHAT_THREADS_PAGE_SIZE,
|
||||
after: endCursor,
|
||||
},
|
||||
},
|
||||
updateQuery: (previousResult, { fetchMoreResult }) => {
|
||||
const newEdges = fetchMoreResult?.chatThreads?.edges ?? [];
|
||||
if (newEdges.length === 0) {
|
||||
return previousResult;
|
||||
}
|
||||
|
||||
return {
|
||||
chatThreads: {
|
||||
...fetchMoreResult.chatThreads,
|
||||
edges: [...(previousResult.chatThreads?.edges ?? []), ...newEdges],
|
||||
pageInfo:
|
||||
fetchMoreResult.chatThreads?.pageInfo ??
|
||||
previousResult.chatThreads?.pageInfo,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
}, [hasNextPage, loading, endCursor, fetchMore]);
|
||||
|
||||
useEffect(() => {
|
||||
if (inView && hasNextPage && !loading && !shouldFetchMore) {
|
||||
setShouldFetchMore(true);
|
||||
const promise = loadMore();
|
||||
if (isDefined(promise)) {
|
||||
promise.finally(() => setShouldFetchMore(false));
|
||||
} else {
|
||||
setShouldFetchMore(false);
|
||||
}
|
||||
}
|
||||
}, [inView, hasNextPage, loading, shouldFetchMore, loadMore]);
|
||||
|
||||
return {
|
||||
threads,
|
||||
hasNextPage,
|
||||
loading,
|
||||
fetchMoreRef,
|
||||
};
|
||||
};
|
||||
@@ -1,12 +1,22 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
|
||||
import { CHAT_THREADS_PAGE_SIZE } from '@/ai/constants/ChatThreads';
|
||||
import { agentChatUsageState } from '@/ai/states/agentChatUsageState';
|
||||
import { currentAIChatThreadState } from '@/ai/states/currentAIChatThreadState';
|
||||
import { currentAIChatThreadTitleState } from '@/ai/states/currentAIChatThreadTitleState';
|
||||
import { useOpenAskAIPageInCommandMenu } from '@/command-menu/hooks/useOpenAskAIPageInCommandMenu';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { useCreateChatThreadMutation } from '~/generated-metadata/graphql';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
type GetChatThreadsQuery,
|
||||
GetChatThreadsDocument,
|
||||
useCreateChatThreadMutation,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export const useCreateNewAIChatThread = () => {
|
||||
const apolloClient = useApolloClient();
|
||||
const [, setCurrentAIChatThread] = useAtomState(currentAIChatThreadState);
|
||||
const setAgentChatUsage = useSetAtomState(agentChatUsageState);
|
||||
const setCurrentAIChatThreadTitle = useSetAtomState(
|
||||
@@ -20,6 +30,42 @@ export const useCreateNewAIChatThread = () => {
|
||||
setCurrentAIChatThreadTitle(null);
|
||||
setAgentChatUsage(null);
|
||||
openAskAIPage({ resetNavigationStack: false });
|
||||
|
||||
const newThread = data.createChatThread;
|
||||
const threadListVariables = {
|
||||
paging: { first: CHAT_THREADS_PAGE_SIZE },
|
||||
};
|
||||
const existing = apolloClient.cache.readQuery<GetChatThreadsQuery>({
|
||||
query: GetChatThreadsDocument,
|
||||
variables: threadListVariables,
|
||||
});
|
||||
if (isDefined(existing) && isDefined(existing.chatThreads)) {
|
||||
const newNode = {
|
||||
__typename: 'AgentChatThread' as const,
|
||||
...newThread,
|
||||
totalInputTokens: 0,
|
||||
totalOutputTokens: 0,
|
||||
contextWindowTokens: null,
|
||||
conversationSize: 0,
|
||||
totalInputCredits: 0,
|
||||
totalOutputCredits: 0,
|
||||
};
|
||||
const newEdge = {
|
||||
__typename: 'AgentChatThreadEdge' as const,
|
||||
node: newNode,
|
||||
cursor: newThread.id,
|
||||
};
|
||||
apolloClient.cache.writeQuery({
|
||||
query: GetChatThreadsDocument,
|
||||
variables: threadListVariables,
|
||||
data: {
|
||||
chatThreads: {
|
||||
...existing.chatThreads,
|
||||
edges: [newEdge, ...existing.chatThreads.edges],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+1
@@ -32,6 +32,7 @@ const StyledAnimatedContainer = styled.div<{
|
||||
isExpanded: boolean;
|
||||
isResizing: boolean;
|
||||
}>`
|
||||
height: 100vh;
|
||||
max-height: 100vh;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
+12
-2
@@ -13,7 +13,7 @@ import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomState
|
||||
import { NavigationDrawerCollapseButton } from './NavigationDrawerCollapseButton';
|
||||
|
||||
const StyledContainer = styled.div<{ isExpanded: boolean }>`
|
||||
align-items: center;
|
||||
align-items: ${({ isExpanded }) => (isExpanded ? 'center' : 'flex-start')};
|
||||
display: flex;
|
||||
flex-direction: ${({ isExpanded }) => (isExpanded ? 'row' : 'column')};
|
||||
gap: ${({ isExpanded }) => (isExpanded ? '0' : themeCssVariables.spacing[4])};
|
||||
@@ -25,6 +25,7 @@ const StyledContainer = styled.div<{ isExpanded: boolean }>`
|
||||
|
||||
const StyledRightActions = styled.div<{ isExpanded: boolean }>`
|
||||
align-items: center;
|
||||
align-self: ${({ isExpanded }) => (isExpanded ? 'auto' : 'flex-end')};
|
||||
display: flex;
|
||||
flex-direction: ${({ isExpanded }) => (isExpanded ? 'row' : 'column')};
|
||||
gap: ${({ isExpanded }) => (isExpanded ? '0' : themeCssVariables.spacing[1])};
|
||||
@@ -40,6 +41,13 @@ const StyledNavigationDrawerCollapseButton = styled(
|
||||
width: ${themeCssVariables.spacing[6]};
|
||||
`;
|
||||
|
||||
const StyledWorkspaceDropdownContainer = styled.div`
|
||||
min-height: ${themeCssVariables.spacing[8]};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
type NavigationDrawerHeaderProps = {
|
||||
showCollapseButton: boolean;
|
||||
};
|
||||
@@ -55,7 +63,9 @@ export const NavigationDrawerHeader = ({
|
||||
|
||||
return (
|
||||
<StyledContainer isExpanded={isNavigationDrawerExpanded}>
|
||||
<MultiWorkspaceDropdownButton />
|
||||
<StyledWorkspaceDropdownContainer>
|
||||
<MultiWorkspaceDropdownButton />
|
||||
</StyledWorkspaceDropdownContainer>
|
||||
{!isMobile && (
|
||||
<StyledRightActions isExpanded={isNavigationDrawerExpanded}>
|
||||
<LightIconButton
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { SortDirection } from '@ptc-org/nestjs-query-core';
|
||||
import {
|
||||
NestjsQueryGraphQLModule,
|
||||
PagingStrategies,
|
||||
} from '@ptc-org/nestjs-query-graphql';
|
||||
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
|
||||
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
|
||||
import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module';
|
||||
@@ -8,14 +16,17 @@ import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-
|
||||
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
|
||||
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
|
||||
import { FileModule } from 'src/engine/core-modules/file/file.module';
|
||||
import { SkillModule } from 'src/engine/metadata-modules/skill/skill.module';
|
||||
import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.module';
|
||||
import { ToolProviderModule } from 'src/engine/core-modules/tool-provider/tool-provider.module';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
|
||||
import { FeatureFlagGuard } from 'src/engine/guards/feature-flag.guard';
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { AiAgentExecutionModule } from 'src/engine/metadata-modules/ai/ai-agent-execution/ai-agent-execution.module';
|
||||
import { AiBillingModule } from 'src/engine/metadata-modules/ai/ai-billing/ai-billing.module';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { SkillModule } from 'src/engine/metadata-modules/skill/skill.module';
|
||||
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
|
||||
@@ -23,6 +34,7 @@ import { DashboardToolsModule } from 'src/modules/dashboard/tools/dashboard-tool
|
||||
import { WorkflowToolsModule } from 'src/modules/workflow/workflow-tools/workflow-tools.module';
|
||||
|
||||
import { AgentChatController } from './controllers/agent-chat.controller';
|
||||
import { AgentChatThreadDTO } from './dtos/agent-chat-thread.dto';
|
||||
import { AgentChatThreadEntity } from './entities/agent-chat-thread.entity';
|
||||
import { AgentChatResolver } from './resolvers/agent-chat.resolver';
|
||||
import { AgentChatStreamingService } from './services/agent-chat-streaming.service';
|
||||
@@ -38,6 +50,35 @@ import { SystemPromptBuilderService } from './services/system-prompt-builder.ser
|
||||
FileEntity,
|
||||
UserWorkspaceEntity,
|
||||
]),
|
||||
NestjsQueryGraphQLModule.forFeature({
|
||||
imports: [
|
||||
NestjsQueryTypeOrmModule.forFeature([AgentChatThreadEntity]),
|
||||
FeatureFlagModule,
|
||||
PermissionsModule,
|
||||
],
|
||||
resolvers: [
|
||||
{
|
||||
EntityClass: AgentChatThreadEntity,
|
||||
DTOClass: AgentChatThreadDTO,
|
||||
pagingStrategy: PagingStrategies.CURSOR,
|
||||
read: {
|
||||
defaultSort: [
|
||||
{ field: 'updatedAt', direction: SortDirection.DESC },
|
||||
],
|
||||
one: { disabled: true },
|
||||
many: { name: 'chatThreads' },
|
||||
},
|
||||
create: { disabled: true },
|
||||
update: { disabled: true },
|
||||
delete: { disabled: true },
|
||||
guards: [
|
||||
WorkspaceAuthGuard,
|
||||
FeatureFlagGuard,
|
||||
SettingsPermissionGuard(PermissionFlagType.AI),
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
AiAgentExecutionModule,
|
||||
BillingModule,
|
||||
ThrottlerModule,
|
||||
|
||||
+28
-2
@@ -1,10 +1,32 @@
|
||||
import { Field, Float, Int, ObjectType } from '@nestjs/graphql';
|
||||
import { UnauthorizedException } from '@nestjs/common';
|
||||
import { Field, Float, HideField, Int, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
Authorize,
|
||||
FilterableField,
|
||||
IDField,
|
||||
} from '@ptc-org/nestjs-query-graphql';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { type AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
|
||||
@ObjectType('AgentChatThread')
|
||||
@Authorize({
|
||||
authorize: (context: { req?: AuthenticatedRequest }) => {
|
||||
const userWorkspaceId = context?.req?.userWorkspaceId;
|
||||
|
||||
if (!isDefined(userWorkspaceId)) {
|
||||
throw new UnauthorizedException(
|
||||
'userWorkspaceId is required to query chat threads',
|
||||
);
|
||||
}
|
||||
|
||||
return { userWorkspaceId: { eq: userWorkspaceId } };
|
||||
},
|
||||
})
|
||||
export class AgentChatThreadDTO {
|
||||
@Field(() => UUIDScalarType)
|
||||
@IDField(() => UUIDScalarType)
|
||||
id: string;
|
||||
|
||||
@Field({ nullable: true })
|
||||
@@ -33,6 +55,10 @@ export class AgentChatThreadDTO {
|
||||
@Field()
|
||||
createdAt: Date;
|
||||
|
||||
@FilterableField()
|
||||
@Field()
|
||||
updatedAt: Date;
|
||||
|
||||
@HideField()
|
||||
userWorkspaceId: string;
|
||||
}
|
||||
|
||||
+1
-7
@@ -11,12 +11,12 @@ import {
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import { FeatureFlagKey } from 'twenty-shared/types';
|
||||
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { toDisplayCredits } from 'src/engine/core-modules/billing/utils/to-display-credits.util';
|
||||
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import {
|
||||
FeatureFlagGuard,
|
||||
RequireFeatureFlag,
|
||||
@@ -42,12 +42,6 @@ export class AgentChatResolver {
|
||||
private readonly systemPromptBuilderService: SystemPromptBuilderService,
|
||||
) {}
|
||||
|
||||
@Query(() => [AgentChatThreadDTO])
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_AI_ENABLED)
|
||||
async chatThreads(@AuthUserWorkspaceId() userWorkspaceId: string) {
|
||||
return this.agentChatService.getThreadsForUser(userWorkspaceId);
|
||||
}
|
||||
|
||||
@Query(() => AgentChatThreadDTO)
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_AI_ENABLED)
|
||||
async chatThread(
|
||||
|
||||
-9
@@ -43,15 +43,6 @@ export class AgentChatService {
|
||||
return this.threadRepository.save(thread);
|
||||
}
|
||||
|
||||
async getThreadsForUser(userWorkspaceId: string) {
|
||||
return this.threadRepository.find({
|
||||
where: {
|
||||
userWorkspaceId,
|
||||
},
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
async getThreadById(threadId: string, userWorkspaceId: string) {
|
||||
const thread = await this.threadRepository.findOne({
|
||||
where: {
|
||||
|
||||
Reference in New Issue
Block a user