feat(wren-ui): Integrating home & home sidebar (#55)

* feat(wren-ui): add home API schema

* feat(wren-ui): integrating query thread to sidebar

* feat(wren-ui): integrating asking task & thread response to home & home/:id

* feat(wren-ui): refine home flow & add thread rename, delete

* fix(wren-ui): response detail steps null error in nested resolver

* style(wren-ui): refine home components style

* style(wren-ui): change no result icon color to gold-6

* refactor(wren-ui): extract asking task to hook & refine thread components arrangement & add error alert to thread response

* feat(wren-ui): add error handlers to askingTask & thread mutation

* fix(wren-ui): fix effect issue when router redirect after thread delete

* fix(wren-ui): fix enum to uppercase & remove unused after rebase conflict

* fix(wren-ui): add status to cancel ask API body & fix 422 error

* chore(wren-ui): extract AI service error detail to logger

---------

Co-authored-by: shimin <shimin@shimins-MacBook-Pro.local>
This commit is contained in:
Shimin
2024-04-02 15:49:55 +08:00
committed by GitHub
co-authored by shimin
parent 3ae0f88de9
commit 8d69cbbe67
22 changed files with 1313 additions and 322 deletions
@@ -14,6 +14,32 @@ export type Scalars = {
JSON: any;
};
export type AskQuestionResponse = {
__typename?: 'AskQuestionResponse';
questions: Array<Maybe<Scalars['String']>>;
};
export type AskingTask = {
__typename?: 'AskingTask';
candidates: Array<ResultCandidate>;
error?: Maybe<Error>;
status: AskingTaskStatus;
};
export type AskingTaskInput = {
question: Scalars['String'];
threadId?: InputMaybe<Scalars['Int']>;
};
export enum AskingTaskStatus {
FAILED = 'FAILED',
FINISHED = 'FINISHED',
GENERATING = 'GENERATING',
SEARCHING = 'SEARCHING',
STOPPED = 'STOPPED',
UNDERSTANDING = 'UNDERSTANDING'
}
export type CalculatedFieldInput = {
diagram?: InputMaybe<Scalars['JSON']>;
expression: Scalars['String'];
@@ -59,6 +85,18 @@ export type CreateSimpleMetricInput = {
timeGrain: Array<TimeGrainInput>;
};
export type CreateThreadInput = {
question: Scalars['String'];
sql: Scalars['String'];
summary: Scalars['String'];
};
export type CreateThreadResponseInput = {
question: Scalars['String'];
sql: Scalars['String'];
summary: Scalars['String'];
};
export type CustomFieldInput = {
expression: Scalars['String'];
name: Scalars['String'];
@@ -80,6 +118,13 @@ export enum DataSourceName {
DUCKDB = 'DUCKDB'
}
export type DetailStep = {
__typename?: 'DetailStep';
cteName?: Maybe<Scalars['String']>;
sql: Scalars['String'];
summary: Scalars['String'];
};
export type DetailedColumn = {
__typename?: 'DetailedColumn';
displayName: Scalars['String'];
@@ -117,6 +162,14 @@ export type DetailedRelation = {
type: RelationType;
};
export type DetailedThread = {
__typename?: 'DetailedThread';
id: Scalars['Int'];
responses: Array<ThreadResponse>;
sql: Scalars['String'];
summary: Scalars['String'];
};
export type Diagram = {
__typename?: 'Diagram';
models: Array<Maybe<DiagramModel>>;
@@ -175,6 +228,14 @@ export type DimensionInput = {
type: Scalars['String'];
};
export type Error = {
__typename?: 'Error';
code?: Maybe<Scalars['String']>;
message?: Maybe<Scalars['String']>;
shortMessage?: Maybe<Scalars['String']>;
stacktrace?: Maybe<Array<Maybe<Scalars['String']>>>;
};
export type FieldInfo = {
__typename?: 'FieldInfo';
displayName: Scalars['String'];
@@ -220,14 +281,31 @@ export type ModelWhereInput = {
export type Mutation = {
__typename?: 'Mutation';
cancelAskingTask: Scalars['Boolean'];
createAskingTask: Task;
createModel: Scalars['JSON'];
createThread: Thread;
createThreadResponse: ThreadResponse;
deleteModel: Scalars['Boolean'];
deleteThread: Scalars['Boolean'];
deploy: Scalars['JSON'];
previewData: Scalars['JSON'];
saveDataSource: DataSource;
saveRelations: Scalars['JSON'];
saveTables: Scalars['JSON'];
startSampleDataset: Scalars['JSON'];
updateModel: Scalars['JSON'];
updateThread: Thread;
};
export type MutationCancelAskingTaskArgs = {
taskId: Scalars['String'];
};
export type MutationCreateAskingTaskArgs = {
data: AskingTaskInput;
};
@@ -236,11 +314,32 @@ export type MutationCreateModelArgs = {
};
export type MutationCreateThreadArgs = {
data: CreateThreadInput;
};
export type MutationCreateThreadResponseArgs = {
data: CreateThreadResponseInput;
threadId: Scalars['Int'];
};
export type MutationDeleteModelArgs = {
where: ModelWhereInput;
};
export type MutationDeleteThreadArgs = {
where: ThreadUniqueWhereInput;
};
export type MutationPreviewDataArgs = {
where: PreviewDataInput;
};
export type MutationSaveDataSourceArgs = {
data: DataSourceInput;
};
@@ -266,6 +365,12 @@ export type MutationUpdateModelArgs = {
where: ModelWhereInput;
};
export type MutationUpdateThreadArgs = {
data: UpdateThreadInput;
where: ThreadUniqueWhereInput;
};
export enum NodeType {
CALCULATED_FIELD = 'CALCULATED_FIELD',
FIELD = 'FIELD',
@@ -287,8 +392,15 @@ export type OnboardingStatusResponse = {
status?: Maybe<OnboardingStatus>;
};
export type PreviewDataInput = {
responseId: Scalars['Int'];
stepIndex?: InputMaybe<Scalars['Int']>;
};
export type Query = {
__typename?: 'Query';
askQuestions: AskQuestionResponse;
askingTask: AskingTask;
autoGenerateRelation?: Maybe<Array<RecommandRelations>>;
diagram: Diagram;
listDataSourceTables: Array<CompactTable>;
@@ -296,6 +408,14 @@ export type Query = {
model: DetailedModel;
modelSync?: Maybe<ModelSyncResponse>;
onboardingStatus: OnboardingStatusResponse;
thread: DetailedThread;
threadResponse: ThreadResponse;
threads: Array<Thread>;
};
export type QueryAskingTaskArgs = {
taskId: Scalars['String'];
};
@@ -303,6 +423,16 @@ export type QueryModelArgs = {
where: ModelWhereInput;
};
export type QueryThreadArgs = {
threadId: Scalars['Int'];
};
export type QueryThreadResponseArgs = {
responseId: Scalars['Int'];
};
export type RecommandRelations = {
__typename?: 'RecommandRelations';
id: Scalars['Int'];
@@ -339,6 +469,12 @@ export enum RelationType {
ONE_TO_ONE = 'ONE_TO_ONE'
}
export type ResultCandidate = {
__typename?: 'ResultCandidate';
sql: Scalars['String'];
summary: Scalars['String'];
};
export type SampleDatasetInput = {
name: SampleDatasetName;
};
@@ -365,6 +501,38 @@ export type SimpleMeasureInput = {
type: Scalars['String'];
};
export type Task = {
__typename?: 'Task';
id: Scalars['String'];
};
export type Thread = {
__typename?: 'Thread';
id: Scalars['Int'];
sql: Scalars['String'];
summary: Scalars['String'];
};
export type ThreadResponse = {
__typename?: 'ThreadResponse';
detail?: Maybe<ThreadResponseDetail>;
error?: Maybe<Error>;
id: Scalars['Int'];
question: Scalars['String'];
status: AskingTaskStatus;
};
export type ThreadResponseDetail = {
__typename?: 'ThreadResponseDetail';
description?: Maybe<Scalars['String']>;
sql?: Maybe<Scalars['String']>;
steps: Array<DetailStep>;
};
export type ThreadUniqueWhereInput = {
id: Scalars['Int'];
};
export type TimeGrainInput = {
dateParts: Array<Scalars['String']>;
name: Scalars['String'];
@@ -379,3 +547,7 @@ export type UpdateModelInput = {
fields: Array<Scalars['String']>;
refreshTime?: InputMaybe<Scalars['String']>;
};
export type UpdateThreadInput = {
summary?: InputMaybe<Scalars['String']>;
};
@@ -0,0 +1,470 @@
import * as Types from './__types__';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type CommonErrorFragment = { __typename?: 'Error', code?: string | null, shortMessage?: string | null, message?: string | null, stacktrace?: Array<string | null> | null };
export type CommonResponseFragment = { __typename?: 'ThreadResponse', id: number, question: string, status: Types.AskingTaskStatus, detail?: { __typename?: 'ThreadResponseDetail', sql?: string | null, description?: string | null, steps: Array<{ __typename?: 'DetailStep', summary: string, sql: string, cteName?: string | null }> } | null };
export type AskingTaskQueryVariables = Types.Exact<{
taskId: Types.Scalars['String'];
}>;
export type AskingTaskQuery = { __typename?: 'Query', askingTask: { __typename?: 'AskingTask', status: Types.AskingTaskStatus, candidates: Array<{ __typename?: 'ResultCandidate', sql: string, summary: string }>, error?: { __typename?: 'Error', code?: string | null, shortMessage?: string | null, message?: string | null, stacktrace?: Array<string | null> | null } | null } };
export type ThreadsQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type ThreadsQuery = { __typename?: 'Query', threads: Array<{ __typename?: 'Thread', id: number, sql: string, summary: string }> };
export type ThreadQueryVariables = Types.Exact<{
threadId: Types.Scalars['Int'];
}>;
export type ThreadQuery = { __typename?: 'Query', thread: { __typename?: 'DetailedThread', id: number, sql: string, summary: string, responses: Array<{ __typename?: 'ThreadResponse', id: number, question: string, status: Types.AskingTaskStatus, error?: { __typename?: 'Error', code?: string | null, shortMessage?: string | null, message?: string | null, stacktrace?: Array<string | null> | null } | null, detail?: { __typename?: 'ThreadResponseDetail', sql?: string | null, description?: string | null, steps: Array<{ __typename?: 'DetailStep', summary: string, sql: string, cteName?: string | null }> } | null }> } };
export type ThreadResponseQueryVariables = Types.Exact<{
responseId: Types.Scalars['Int'];
}>;
export type ThreadResponseQuery = { __typename?: 'Query', threadResponse: { __typename?: 'ThreadResponse', id: number, question: string, status: Types.AskingTaskStatus, error?: { __typename?: 'Error', code?: string | null, shortMessage?: string | null, message?: string | null, stacktrace?: Array<string | null> | null } | null, detail?: { __typename?: 'ThreadResponseDetail', sql?: string | null, description?: string | null, steps: Array<{ __typename?: 'DetailStep', summary: string, sql: string, cteName?: string | null }> } | null } };
export type CreateAskingTaskMutationVariables = Types.Exact<{
data: Types.AskingTaskInput;
}>;
export type CreateAskingTaskMutation = { __typename?: 'Mutation', createAskingTask: { __typename?: 'Task', id: string } };
export type CancelAskingTaskMutationVariables = Types.Exact<{
taskId: Types.Scalars['String'];
}>;
export type CancelAskingTaskMutation = { __typename?: 'Mutation', cancelAskingTask: boolean };
export type CreateThreadMutationVariables = Types.Exact<{
data: Types.CreateThreadInput;
}>;
export type CreateThreadMutation = { __typename?: 'Mutation', createThread: { __typename?: 'Thread', id: number, sql: string, summary: string } };
export type CreateThreadResponseMutationVariables = Types.Exact<{
threadId: Types.Scalars['Int'];
data: Types.CreateThreadResponseInput;
}>;
export type CreateThreadResponseMutation = { __typename?: 'Mutation', createThreadResponse: { __typename?: 'ThreadResponse', id: number, question: string, status: Types.AskingTaskStatus, error?: { __typename?: 'Error', code?: string | null, shortMessage?: string | null, message?: string | null, stacktrace?: Array<string | null> | null } | null, detail?: { __typename?: 'ThreadResponseDetail', sql?: string | null, description?: string | null, steps: Array<{ __typename?: 'DetailStep', summary: string, sql: string, cteName?: string | null }> } | null } };
export type UpdateThreadMutationVariables = Types.Exact<{
where: Types.ThreadUniqueWhereInput;
data: Types.UpdateThreadInput;
}>;
export type UpdateThreadMutation = { __typename?: 'Mutation', updateThread: { __typename?: 'Thread', id: number, sql: string, summary: string } };
export type DeleteThreadMutationVariables = Types.Exact<{
where: Types.ThreadUniqueWhereInput;
}>;
export type DeleteThreadMutation = { __typename?: 'Mutation', deleteThread: boolean };
export const CommonErrorFragmentDoc = gql`
fragment CommonError on Error {
code
shortMessage
message
stacktrace
}
`;
export const CommonResponseFragmentDoc = gql`
fragment CommonResponse on ThreadResponse {
id
question
status
detail {
sql
description
steps {
summary
sql
cteName
}
}
}
`;
export const AskingTaskDocument = gql`
query AskingTask($taskId: String!) {
askingTask(taskId: $taskId) {
status
candidates {
sql
summary
}
error {
...CommonError
}
}
}
${CommonErrorFragmentDoc}`;
/**
* __useAskingTaskQuery__
*
* To run a query within a React component, call `useAskingTaskQuery` and pass it any options that fit your needs.
* When your component renders, `useAskingTaskQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useAskingTaskQuery({
* variables: {
* taskId: // value for 'taskId'
* },
* });
*/
export function useAskingTaskQuery(baseOptions: Apollo.QueryHookOptions<AskingTaskQuery, AskingTaskQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<AskingTaskQuery, AskingTaskQueryVariables>(AskingTaskDocument, options);
}
export function useAskingTaskLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<AskingTaskQuery, AskingTaskQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<AskingTaskQuery, AskingTaskQueryVariables>(AskingTaskDocument, options);
}
export type AskingTaskQueryHookResult = ReturnType<typeof useAskingTaskQuery>;
export type AskingTaskLazyQueryHookResult = ReturnType<typeof useAskingTaskLazyQuery>;
export type AskingTaskQueryResult = Apollo.QueryResult<AskingTaskQuery, AskingTaskQueryVariables>;
export const ThreadsDocument = gql`
query Threads {
threads {
id
sql
summary
}
}
`;
/**
* __useThreadsQuery__
*
* To run a query within a React component, call `useThreadsQuery` and pass it any options that fit your needs.
* When your component renders, `useThreadsQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useThreadsQuery({
* variables: {
* },
* });
*/
export function useThreadsQuery(baseOptions?: Apollo.QueryHookOptions<ThreadsQuery, ThreadsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ThreadsQuery, ThreadsQueryVariables>(ThreadsDocument, options);
}
export function useThreadsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ThreadsQuery, ThreadsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ThreadsQuery, ThreadsQueryVariables>(ThreadsDocument, options);
}
export type ThreadsQueryHookResult = ReturnType<typeof useThreadsQuery>;
export type ThreadsLazyQueryHookResult = ReturnType<typeof useThreadsLazyQuery>;
export type ThreadsQueryResult = Apollo.QueryResult<ThreadsQuery, ThreadsQueryVariables>;
export const ThreadDocument = gql`
query Thread($threadId: Int!) {
thread(threadId: $threadId) {
id
sql
summary
responses {
...CommonResponse
error {
...CommonError
}
}
}
}
${CommonResponseFragmentDoc}
${CommonErrorFragmentDoc}`;
/**
* __useThreadQuery__
*
* To run a query within a React component, call `useThreadQuery` and pass it any options that fit your needs.
* When your component renders, `useThreadQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useThreadQuery({
* variables: {
* threadId: // value for 'threadId'
* },
* });
*/
export function useThreadQuery(baseOptions: Apollo.QueryHookOptions<ThreadQuery, ThreadQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ThreadQuery, ThreadQueryVariables>(ThreadDocument, options);
}
export function useThreadLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ThreadQuery, ThreadQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ThreadQuery, ThreadQueryVariables>(ThreadDocument, options);
}
export type ThreadQueryHookResult = ReturnType<typeof useThreadQuery>;
export type ThreadLazyQueryHookResult = ReturnType<typeof useThreadLazyQuery>;
export type ThreadQueryResult = Apollo.QueryResult<ThreadQuery, ThreadQueryVariables>;
export const ThreadResponseDocument = gql`
query ThreadResponse($responseId: Int!) {
threadResponse(responseId: $responseId) {
...CommonResponse
error {
...CommonError
}
}
}
${CommonResponseFragmentDoc}
${CommonErrorFragmentDoc}`;
/**
* __useThreadResponseQuery__
*
* To run a query within a React component, call `useThreadResponseQuery` and pass it any options that fit your needs.
* When your component renders, `useThreadResponseQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useThreadResponseQuery({
* variables: {
* responseId: // value for 'responseId'
* },
* });
*/
export function useThreadResponseQuery(baseOptions: Apollo.QueryHookOptions<ThreadResponseQuery, ThreadResponseQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ThreadResponseQuery, ThreadResponseQueryVariables>(ThreadResponseDocument, options);
}
export function useThreadResponseLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ThreadResponseQuery, ThreadResponseQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ThreadResponseQuery, ThreadResponseQueryVariables>(ThreadResponseDocument, options);
}
export type ThreadResponseQueryHookResult = ReturnType<typeof useThreadResponseQuery>;
export type ThreadResponseLazyQueryHookResult = ReturnType<typeof useThreadResponseLazyQuery>;
export type ThreadResponseQueryResult = Apollo.QueryResult<ThreadResponseQuery, ThreadResponseQueryVariables>;
export const CreateAskingTaskDocument = gql`
mutation CreateAskingTask($data: AskingTaskInput!) {
createAskingTask(data: $data) {
id
}
}
`;
export type CreateAskingTaskMutationFn = Apollo.MutationFunction<CreateAskingTaskMutation, CreateAskingTaskMutationVariables>;
/**
* __useCreateAskingTaskMutation__
*
* To run a mutation, you first call `useCreateAskingTaskMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useCreateAskingTaskMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [createAskingTaskMutation, { data, loading, error }] = useCreateAskingTaskMutation({
* variables: {
* data: // value for 'data'
* },
* });
*/
export function useCreateAskingTaskMutation(baseOptions?: Apollo.MutationHookOptions<CreateAskingTaskMutation, CreateAskingTaskMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<CreateAskingTaskMutation, CreateAskingTaskMutationVariables>(CreateAskingTaskDocument, options);
}
export type CreateAskingTaskMutationHookResult = ReturnType<typeof useCreateAskingTaskMutation>;
export type CreateAskingTaskMutationResult = Apollo.MutationResult<CreateAskingTaskMutation>;
export type CreateAskingTaskMutationOptions = Apollo.BaseMutationOptions<CreateAskingTaskMutation, CreateAskingTaskMutationVariables>;
export const CancelAskingTaskDocument = gql`
mutation CancelAskingTask($taskId: String!) {
cancelAskingTask(taskId: $taskId)
}
`;
export type CancelAskingTaskMutationFn = Apollo.MutationFunction<CancelAskingTaskMutation, CancelAskingTaskMutationVariables>;
/**
* __useCancelAskingTaskMutation__
*
* To run a mutation, you first call `useCancelAskingTaskMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useCancelAskingTaskMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [cancelAskingTaskMutation, { data, loading, error }] = useCancelAskingTaskMutation({
* variables: {
* taskId: // value for 'taskId'
* },
* });
*/
export function useCancelAskingTaskMutation(baseOptions?: Apollo.MutationHookOptions<CancelAskingTaskMutation, CancelAskingTaskMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<CancelAskingTaskMutation, CancelAskingTaskMutationVariables>(CancelAskingTaskDocument, options);
}
export type CancelAskingTaskMutationHookResult = ReturnType<typeof useCancelAskingTaskMutation>;
export type CancelAskingTaskMutationResult = Apollo.MutationResult<CancelAskingTaskMutation>;
export type CancelAskingTaskMutationOptions = Apollo.BaseMutationOptions<CancelAskingTaskMutation, CancelAskingTaskMutationVariables>;
export const CreateThreadDocument = gql`
mutation CreateThread($data: CreateThreadInput!) {
createThread(data: $data) {
id
sql
summary
}
}
`;
export type CreateThreadMutationFn = Apollo.MutationFunction<CreateThreadMutation, CreateThreadMutationVariables>;
/**
* __useCreateThreadMutation__
*
* To run a mutation, you first call `useCreateThreadMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useCreateThreadMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [createThreadMutation, { data, loading, error }] = useCreateThreadMutation({
* variables: {
* data: // value for 'data'
* },
* });
*/
export function useCreateThreadMutation(baseOptions?: Apollo.MutationHookOptions<CreateThreadMutation, CreateThreadMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<CreateThreadMutation, CreateThreadMutationVariables>(CreateThreadDocument, options);
}
export type CreateThreadMutationHookResult = ReturnType<typeof useCreateThreadMutation>;
export type CreateThreadMutationResult = Apollo.MutationResult<CreateThreadMutation>;
export type CreateThreadMutationOptions = Apollo.BaseMutationOptions<CreateThreadMutation, CreateThreadMutationVariables>;
export const CreateThreadResponseDocument = gql`
mutation CreateThreadResponse($threadId: Int!, $data: CreateThreadResponseInput!) {
createThreadResponse(threadId: $threadId, data: $data) {
...CommonResponse
error {
code
shortMessage
message
stacktrace
}
}
}
${CommonResponseFragmentDoc}`;
export type CreateThreadResponseMutationFn = Apollo.MutationFunction<CreateThreadResponseMutation, CreateThreadResponseMutationVariables>;
/**
* __useCreateThreadResponseMutation__
*
* To run a mutation, you first call `useCreateThreadResponseMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useCreateThreadResponseMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [createThreadResponseMutation, { data, loading, error }] = useCreateThreadResponseMutation({
* variables: {
* threadId: // value for 'threadId'
* data: // value for 'data'
* },
* });
*/
export function useCreateThreadResponseMutation(baseOptions?: Apollo.MutationHookOptions<CreateThreadResponseMutation, CreateThreadResponseMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<CreateThreadResponseMutation, CreateThreadResponseMutationVariables>(CreateThreadResponseDocument, options);
}
export type CreateThreadResponseMutationHookResult = ReturnType<typeof useCreateThreadResponseMutation>;
export type CreateThreadResponseMutationResult = Apollo.MutationResult<CreateThreadResponseMutation>;
export type CreateThreadResponseMutationOptions = Apollo.BaseMutationOptions<CreateThreadResponseMutation, CreateThreadResponseMutationVariables>;
export const UpdateThreadDocument = gql`
mutation UpdateThread($where: ThreadUniqueWhereInput!, $data: UpdateThreadInput!) {
updateThread(where: $where, data: $data) {
id
sql
summary
}
}
`;
export type UpdateThreadMutationFn = Apollo.MutationFunction<UpdateThreadMutation, UpdateThreadMutationVariables>;
/**
* __useUpdateThreadMutation__
*
* To run a mutation, you first call `useUpdateThreadMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useUpdateThreadMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [updateThreadMutation, { data, loading, error }] = useUpdateThreadMutation({
* variables: {
* where: // value for 'where'
* data: // value for 'data'
* },
* });
*/
export function useUpdateThreadMutation(baseOptions?: Apollo.MutationHookOptions<UpdateThreadMutation, UpdateThreadMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<UpdateThreadMutation, UpdateThreadMutationVariables>(UpdateThreadDocument, options);
}
export type UpdateThreadMutationHookResult = ReturnType<typeof useUpdateThreadMutation>;
export type UpdateThreadMutationResult = Apollo.MutationResult<UpdateThreadMutation>;
export type UpdateThreadMutationOptions = Apollo.BaseMutationOptions<UpdateThreadMutation, UpdateThreadMutationVariables>;
export const DeleteThreadDocument = gql`
mutation DeleteThread($where: ThreadUniqueWhereInput!) {
deleteThread(where: $where)
}
`;
export type DeleteThreadMutationFn = Apollo.MutationFunction<DeleteThreadMutation, DeleteThreadMutationVariables>;
/**
* __useDeleteThreadMutation__
*
* To run a mutation, you first call `useDeleteThreadMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useDeleteThreadMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [deleteThreadMutation, { data, loading, error }] = useDeleteThreadMutation({
* variables: {
* where: // value for 'where'
* },
* });
*/
export function useDeleteThreadMutation(baseOptions?: Apollo.MutationHookOptions<DeleteThreadMutation, DeleteThreadMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<DeleteThreadMutation, DeleteThreadMutationVariables>(DeleteThreadDocument, options);
}
export type DeleteThreadMutationHookResult = ReturnType<typeof useDeleteThreadMutation>;
export type DeleteThreadMutationResult = Apollo.MutationResult<DeleteThreadMutation>;
export type DeleteThreadMutationOptions = Apollo.BaseMutationOptions<DeleteThreadMutation, DeleteThreadMutationVariables>;
+146
View File
@@ -0,0 +1,146 @@
import { gql } from '@apollo/client';
const COMMON_ERROR = gql`
fragment CommonError on Error {
code
shortMessage
message
stacktrace
}
`;
const COMMON_RESPONSE = gql`
fragment CommonResponse on ThreadResponse {
id
question
status
detail {
sql
description
steps {
summary
sql
cteName
}
}
}
`;
export const ASKING_TASK = gql`
query AskingTask($taskId: String!) {
askingTask(taskId: $taskId) {
status
candidates {
sql
summary
}
error {
...CommonError
}
}
}
${COMMON_ERROR}
`;
export const THREADS = gql`
query Threads {
threads {
id
sql
summary
}
}
`;
export const THREAD = gql`
query Thread($threadId: Int!) {
thread(threadId: $threadId) {
id
sql
summary
responses {
...CommonResponse
error {
...CommonError
}
}
}
}
${COMMON_RESPONSE}
${COMMON_ERROR}
`;
export const THREAD_RESPONSE = gql`
query ThreadResponse($responseId: Int!) {
threadResponse(responseId: $responseId) {
...CommonResponse
error {
...CommonError
}
}
}
${COMMON_RESPONSE}
${COMMON_ERROR}
`;
export const CREATE_ASKING_TASK = gql`
mutation CreateAskingTask($data: AskingTaskInput!) {
createAskingTask(data: $data) {
id
}
}
`;
export const CANCEL_ASKING_TASK = gql`
mutation CancelAskingTask($taskId: String!) {
cancelAskingTask(taskId: $taskId)
}
`;
export const CREATE_THREAD = gql`
mutation CreateThread($data: CreateThreadInput!) {
createThread(data: $data) {
id
sql
summary
}
}
`;
export const CREATE_THREAD_RESPONSE = gql`
mutation CreateThreadResponse(
$threadId: Int!
$data: CreateThreadResponseInput!
) {
createThreadResponse(threadId: $threadId, data: $data) {
...CommonResponse
error {
code
shortMessage
message
stacktrace
}
}
}
${COMMON_RESPONSE}
${COMMON_ERROR}
`;
export const UPDATE_THREAD = gql`
mutation UpdateThread(
$where: ThreadUniqueWhereInput!
$data: UpdateThreadInput!
) {
updateThread(where: $where, data: $data) {
id
sql
summary
}
}
`;
export const DELETE_THREAD = gql`
mutation DeleteThread($where: ThreadUniqueWhereInput!) {
deleteThread(where: $where)
}
`;
@@ -92,6 +92,13 @@ export type AskResult = AskResponse<
AskResultStatus
>;
const getAISerciceError = (error: any) => {
const { data } = error.response || {};
return data?.detail
? `${error.message}, detail: ${data.detail}`
: error.message;
};
export interface IWrenAIAdaptor {
deploy(deployData: deployData): Promise<WrenAIDeployResponse>;
@@ -136,7 +143,7 @@ export class WrenAIAdaptor implements IWrenAIAdaptor {
});
return { queryId: res.data.query_id };
} catch (err: any) {
logger.debug(`Got error when asking wren AI: ${err.message}`);
logger.debug(`Got error when asking wren AI: ${getAISerciceError(err)}`);
throw err;
}
}
@@ -144,9 +151,11 @@ export class WrenAIAdaptor implements IWrenAIAdaptor {
public async cancelAsk(queryId: string): Promise<void> {
// make PATCH request /v1/asks/:query_id to cancel the query
try {
await axios.patch(`${this.wrenAIBaseEndpoint}/v1/asks/${queryId}`);
await axios.patch(`${this.wrenAIBaseEndpoint}/v1/asks/${queryId}`, {
status: 'stopped',
});
} catch (err: any) {
logger.debug(`Got error when canceling ask: ${err.message}`);
logger.debug(`Got error when canceling ask: ${getAISerciceError(err)}`);
throw err;
}
}
@@ -159,7 +168,9 @@ export class WrenAIAdaptor implements IWrenAIAdaptor {
);
return this.transformAskResult(res.data);
} catch (err: any) {
logger.debug(`Got error when getting ask result: ${err.message}`);
logger.debug(
`Got error when getting ask result: ${getAISerciceError(err)}`,
);
// throw err;
throw Errors.create(Errors.GeneralErrorCodes.INTERNAL_SERVER_ERROR, {
originalError: err,
@@ -181,7 +192,9 @@ export class WrenAIAdaptor implements IWrenAIAdaptor {
);
return { queryId: res.data.query_id };
} catch (err: any) {
logger.debug(`Got error when generating ask detail: ${err.message}`);
logger.debug(
`Got error when generating ask detail: ${getAISerciceError(err)}`,
);
throw err;
}
}
@@ -194,7 +207,9 @@ export class WrenAIAdaptor implements IWrenAIAdaptor {
);
return this.transformAskDetailResult(res.data);
} catch (err: any) {
logger.debug(`Got error when getting ask detail result: ${err.message}`);
logger.debug(
`Got error when getting ask detail result: ${getAISerciceError(err)}`,
);
throw err;
}
}
@@ -222,7 +237,7 @@ export class WrenAIAdaptor implements IWrenAIAdaptor {
}
} catch (err: any) {
logger.debug(
`Got error when deploying to wren AI, hash: ${hash}. Error: ${err.message}`,
`Got error when deploying to wren AI, hash: ${hash}. Error: ${getAISerciceError(err)}`,
);
return {
status: WrenAIDeployStatusEnum.FAILED,
@@ -269,7 +284,7 @@ export class WrenAIAdaptor implements IWrenAIAdaptor {
return res.data?.status.toUpperCase() as WrenAISystemStatus;
} catch (err: any) {
logger.debug(
`Got error in API /v1/semantics-preparations/${deployId}/status: ${err.message}`,
`Got error in API /v1/semantics-preparations/${deployId}/status: ${getAISerciceError(err)}`,
);
throw err;
}
@@ -247,10 +247,12 @@ export class AskingResolver {
public getThreadResponseNestedResolver = () => ({
detail: (parent: ThreadResponse, _args: any, _ctx: IContext) => {
// extend sql to detail
return {
...parent.detail,
sql: format(constructCteSql(parent.detail.steps)),
};
return parent.detail
? {
...parent.detail,
sql: format(constructCteSql(parent.detail.steps)),
}
: null;
},
});
+17 -11
View File
@@ -6,20 +6,24 @@ import '@/components/editor/AceEditor';
const Block = styled.div<{ inline?: boolean }>`
position: relative;
white-space: pre;
overflow-x: auto;
${(props) =>
props.inline
? `display: inline; border: none; background: transparent !important; padding: 0;`
: `background: var(--gray-1); padding: 4px;`}
.line-number {
user-select: none;
display: inline-block;
min-width: 1.5em;
text-align: right;
margin-right: 1em;
color: var(--gray-6);
font-weight: 700;
font-size: 14px;
.adm-code-line {
${(props) => (props.inline ? '' : 'display: block;')}
&-number {
user-select: none;
display: inline-block;
min-width: 1.5em;
text-align: right;
margin-right: 1em;
color: var(--gray-6);
font-weight: 700;
font-size: 14px;
}
}
`;
@@ -77,8 +81,10 @@ export default function CodeBlock(props: Props) {
});
return (
<span className="ace_line" key={`${line}-${index}`}>
{showLineNumbers && <span className="line-number">{index + 1}</span>}
<span className="adm-code-line ace_line" key={`${line}-${index}`}>
{showLineNumbers && (
<span className="adm-code-line-number">{index + 1}</span>
)}
{children}
</span>
);
@@ -15,6 +15,7 @@ import ViewSQLModal from '@/components/pages/home/prompt/ViewSQLModal';
import EllipsisWrapper from '@/components/EllipsisWrapper';
import useModalAction from '@/hooks/useModalAction';
import useAskProcessState from '@/hooks/useAskProcessState';
import { AskingTask } from '@/apollo/client/graphql/__types__';
const ResultStyle = styled.div`
position: absolute;
@@ -52,16 +53,20 @@ const ResultBlock = styled.div`
interface Props {
processState: ReturnType<typeof useAskProcessState>;
data: any[];
data: AskingTask['candidates'];
error?: any;
onSelect: (payload: { sql: string; summary: string }) => void;
onClose: () => void;
onStop: () => void;
}
const ResultTemplate = ({ index, summary, sql, onShowSQL }) => {
const ResultTemplate = ({ index, summary, sql, onSelect, onShowSQL }) => {
return (
<Col span={8}>
<ResultBlock className="border border-gray-5 rounded px-3 pt-3 pb-4 cursor-pointer">
<ResultBlock
className="border border-gray-5 rounded px-3 pt-3 pb-4 cursor-pointer"
onClick={() => onSelect({ sql, summary })}
>
<div className="d-flex justify-space-between align-center text-sm mb-3">
<div className="border border-gray-5 px-2 rounded-pill">
Result {index + 1}
@@ -69,7 +74,7 @@ const ResultTemplate = ({ index, summary, sql, onShowSQL }) => {
<Button
className="adm-btn-no-style gray-6 text-sm px-1"
type="text"
onClick={() => onShowSQL({ sql, summary })}
onClick={(event) => onShowSQL(event, { sql, summary })}
>
<FunctionOutlined className="-mr-1" />
View SQL
@@ -104,16 +109,17 @@ const makeProcessing = (text: string) => (props: Props) => {
};
const makeProcessingError =
(config: { icon: ReactNode; title: string; description: string }) =>
(config: { icon: ReactNode; title?: string; description?: string }) =>
(props: Props) => {
const { onClose, error } = props;
const { message } = error || {};
const { message, shortMessage, stacktrace } = error || {};
const hasStacktrace = !!stacktrace;
return (
<div>
<div className="d-flex justify-space-between text-medium mb-2">
<div className="d-flex align-center">
{config.icon}
{config.title}
{config.title || shortMessage}
</div>
<Button
className="adm-btn-no-style gray-7 bg-gray-3 text-sm px-2"
@@ -125,8 +131,8 @@ const makeProcessingError =
Close
</Button>
</div>
<div className="gray-7">{config.description}</div>
{message && (
<div className="gray-7">{config.description || message}</div>
{hasStacktrace && (
<StyledCollapse
ghost
expandIcon={({ isActive }) => (
@@ -134,7 +140,7 @@ const makeProcessingError =
)}
>
<Collapse.Panel key="1" header="Show error messages">
<pre className="mb-0">{message}</pre>
<pre className="mb-0">{stacktrace.join('\n')}</pre>
</Collapse.Panel>
</StyledCollapse>
)}
@@ -144,22 +150,12 @@ const makeProcessingError =
const ErrorIcon = () => <CloseCircleFilled className="mr-2 red-5 text-lg" />;
const UnderstandingFailed = makeProcessingError({
const Failed = makeProcessingError({
icon: <ErrorIcon />,
title: 'Failed to understand',
description:
'Sorry, I cannot understand your question. Could you please rephrase your question or provide more context?',
});
const SearchingFailed = makeProcessingError({
icon: <ErrorIcon />,
title: 'Something went wrong',
description:
"Sorry, we encountered an error when we're processing your request.",
});
const NoResult = makeProcessingError({
icon: <WarningOutlined className="mr-2 text-lg" />,
icon: <WarningOutlined className="mr-2 text-lg gold-6" />,
title: 'Please try again',
description: 'No results found. Try providing more details in your question.',
});
@@ -167,14 +163,21 @@ const NoResult = makeProcessingError({
const Understanding = makeProcessing('Understanding question');
const Searching = makeProcessing('Searching data');
const Finished = (props: Props) => {
const { data, onClose } = props;
const { data, onClose, onSelect } = props;
const viewSQLModal = useModalAction();
const showSQL = (payload: { sql: string; summary: string }) => {
const showSQL = (event, payload: { sql: string; summary: string }) => {
viewSQLModal.openModal(payload);
event.stopPropagation();
};
const selectResult = (payload: { sql: string; summary: string }) => {
onSelect && onSelect(payload);
};
if (data.length === 0) return <NoResult {...props} />;
return (
<div>
<div className="d-flex justify-space-between mb-3">
@@ -193,7 +196,11 @@ const Finished = (props: Props) => {
</Button>
</div>
<Row gutter={12}>
<ResultColumnIterator data={data} onShowSQL={showSQL} />
<ResultColumnIterator
data={data}
onShowSQL={showSQL}
onSelect={selectResult}
/>
</Row>
<ViewSQLModal {...viewSQLModal.state} onClose={viewSQLModal.closeModal} />
</div>
@@ -205,10 +212,10 @@ const getProcessStateComponent = (state: PROCESS_STATE) => {
{
[PROCESS_STATE.UNDERSTANDING]: Understanding,
[PROCESS_STATE.SEARCHING]: Searching,
// generating no need to change UI
[PROCESS_STATE.GENERATING]: Searching,
[PROCESS_STATE.FINISHED]: Finished,
[PROCESS_STATE.UNDERSTANDING_FAILED]: UnderstandingFailed,
[PROCESS_STATE.SEARCHING_FAILED]: SearchingFailed,
[PROCESS_STATE.NO_RESULT]: NoResult,
[PROCESS_STATE.FAILED]: Failed,
}[state] || null
);
};
@@ -1,77 +1,93 @@
import { useEffect, useMemo, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { Input, Button } from 'antd';
import styled from 'styled-components';
import { PROCESS_STATE } from '@/utils/enum';
import PromptResult from '@/components/pages/home/prompt/Result';
import useAskProcessState from '@/hooks/useAskProcessState';
import useAskProcessState, {
getIsProcessing,
} from '@/hooks/useAskProcessState';
import {
AskingTask,
AskingTaskStatus,
} from '@/apollo/client/graphql/__types__';
interface Props {
onSelect: (payload: {
sql: string;
summary: string;
question: string;
}) => void;
onStop: () => void;
onSubmit: (value: string) => void;
data?: { status: string; result: { sql: string; summary: string }[] };
error?: any;
data?: AskingTask;
}
const PromptStyle = styled.div`
position: fixed;
width: 768px;
left: 50%;
margin-left: calc(-384px + 140px);
margin-left: calc(-384px + 133px);
bottom: 12px;
z-index: 999;
`;
const PromptButton = styled(Button)`
min-width: 72px;
`;
const convertToProcessState = (status: string) => {
return {
understanding: PROCESS_STATE.UNDERSTANDING,
searching: PROCESS_STATE.SEARCHING,
finished: PROCESS_STATE.FINISHED,
}[status];
};
const convertAskingTaskToProcessState = (data: AskingTask) => {
const processState = {
[AskingTaskStatus.UNDERSTANDING]: PROCESS_STATE.UNDERSTANDING,
[AskingTaskStatus.SEARCHING]: PROCESS_STATE.SEARCHING,
[AskingTaskStatus.GENERATING]: PROCESS_STATE.GENERATING,
[AskingTaskStatus.FINISHED]: PROCESS_STATE.FINISHED,
}[data.status];
const convertToErrorState = (status: string) => {
return {
understanding: PROCESS_STATE.UNDERSTANDING_FAILED,
searching: PROCESS_STATE.SEARCHING_FAILED,
finished: PROCESS_STATE.NO_RESULT,
}[status];
if (processState === PROCESS_STATE.FINISHED && data.candidates.length === 0) {
return PROCESS_STATE.NO_RESULT;
}
return processState;
};
export default function Prompt(props: Props) {
const { data, error, onSubmit, onStop } = props;
const $promptInput = useRef<HTMLTextAreaElement>(null);
const { data, onSubmit, onStop, onSelect } = props;
const [inputValue, setInputValue] = useState('');
const askProcessState = useAskProcessState();
const results = useMemo(() => data.result || [], [data]);
const candidates = useMemo(() => data?.candidates || [], [data?.candidates]);
const error = useMemo(() => data?.error || null, [data?.error]);
const question = useMemo(() => inputValue.trim(), [inputValue]);
const isProcessing = useMemo(
() =>
[PROCESS_STATE.UNDERSTANDING, PROCESS_STATE.SEARCHING].includes(
askProcessState.currentState,
),
() => getIsProcessing(askProcessState.currentState),
[askProcessState.currentState],
);
useEffect(() => {
if (!isProcessing) $promptInput.current?.focus();
}, [isProcessing]);
useEffect(() => {
if (data) {
const processState = convertToProcessState(data.status);
const processState = convertAskingTaskToProcessState(data);
askProcessState.setState(processState);
}
}, [data]);
useEffect(() => {
if (error) {
// TODO: confirm error state
const errorState = convertToErrorState(error.status);
askProcessState.setState(errorState);
askProcessState.setState(PROCESS_STATE.FAILED);
}
}, [error]);
const selectResult = (payload) => {
onSelect && onSelect({ ...payload, question });
closeResult();
};
const closeResult = () => {
askProcessState.resetState();
setInputValue('');
};
const stopProcess = () => {
@@ -99,12 +115,14 @@ export default function Prompt(props: Props) {
return (
<PromptStyle className="d-flex align-end bg-gray-2 p-3 border border-gray-3 rounded">
<Input.TextArea
ref={$promptInput}
size="large"
autoSize
placeholder="Ask to explore your data"
value={inputValue}
onInput={syncInputValue}
onPressEnter={inputEnter}
disabled={isProcessing}
/>
<PromptButton
type="primary"
@@ -117,9 +135,10 @@ export default function Prompt(props: Props) {
</PromptButton>
<PromptResult
data={results}
data={candidates}
error={error}
processState={askProcessState}
onSelect={selectResult}
onClose={closeResult}
onStop={stopProcess}
/>
@@ -1,32 +1,23 @@
import { Button, Skeleton, Typography } from 'antd';
import { Skeleton, Typography } from 'antd';
import CheckCircleOutlined from '@ant-design/icons/CheckCircleOutlined';
import SaveOutlined from '@ant-design/icons/SaveOutlined';
import QuestionCircleOutlined from '@ant-design/icons/QuestionCircleOutlined';
import StepContent from '@/components/pages/home/StepContent';
import StepContent from '@/components/pages/home/promptThread/StepContent';
const { Title } = Typography;
interface Props {
loading: boolean;
onOpenSaveAsViewModal: (data: { sql: string }) => void;
question: string;
description: string;
answerResultSteps: Array<{
summary: string;
sql: string;
}>;
sql: string;
fullSql: string;
}
export default function AnswerResult(props: Props) {
const {
loading,
onOpenSaveAsViewModal,
question,
description,
answerResultSteps,
sql,
} = props;
const { loading, question, description, answerResultSteps, fullSql } = props;
return (
<Skeleton active loading={loading}>
@@ -42,25 +33,16 @@ export default function AnswerResult(props: Props) {
<Title level={5} style={{ fontWeight: 400 }}>
{description}
</Title>
{answerResultSteps.map((step, index) => (
{(answerResultSteps || []).map((step, index) => (
<StepContent
isLastStep={index === answerResultSteps.length - 1}
key={`${step.summary}-${index}`}
sql={step.sql}
fullSql={sql}
fullSql={fullSql}
stepNumber={index + 1}
summary={step.summary}
/>
))}
<Button
className="mt-2 gray-6"
type="text"
size="small"
icon={<SaveOutlined />}
onClick={() => onOpenSaveAsViewModal({ sql })}
>
Save as view
</Button>
</Typography>
</Skeleton>
);
@@ -2,7 +2,7 @@ import dynamic from 'next/dynamic';
import { Button, Typography } from 'antd';
import CopyOutlined from '@ant-design/icons/lib/icons/CopyOutlined';
import UpCircleOutlined from '@ant-design/icons/UpCircleOutlined';
import PreviewData from '@/components/pages/home/PreviewData';
import PreviewData from '@/components/pages/home/promptThread/PreviewData';
const CodeBlock = dynamic(() => import('@/components/editor/CodeBlock'), {
ssr: false,
@@ -3,7 +3,7 @@ import FunctionOutlined from '@ant-design/icons/FunctionOutlined';
import { BinocularsIcon } from '@/utils/icons';
import CollapseContent, {
Props as CollapseContentProps,
} from '@/components/pages/home/CollapseContent';
} from '@/components/pages/home/promptThread/CollapseContent';
import useAnswerStepContent from '@/hooks/useAnswerStepContent';
const { Title, Paragraph } = Typography;
@@ -0,0 +1,97 @@
import { useEffect, useRef } from 'react';
import { Alert, Divider } from 'antd';
import styled from 'styled-components';
import AnswerResult from './AnswerResult';
import { makeIterable } from '@/utils/iteration';
import {
AskingTaskStatus,
DetailedThread,
} from '@/apollo/client/graphql/__types__';
interface Props {
data: DetailedThread;
}
const StyledPromptThread = styled.div`
width: 768px;
margin-left: auto;
margin-right: auto;
h4.ant-typography {
margin-top: 10px;
}
.ant-typography pre {
border: none;
border-radius: 4px;
}
.ace_editor {
border: none;
}
button {
vertical-align: middle;
}
`;
const AnswerResultTemplate = ({
index,
id,
status,
question,
detail,
error,
}) => {
return (
<div key={`${id}-${index}`}>
{index > 0 && <Divider />}
{error ? (
<Alert
message={error.shortMessage}
description={error.message}
type="error"
showIcon
/>
) : (
<AnswerResult
answerResultSteps={detail?.steps}
description={detail?.description}
loading={status !== AskingTaskStatus.FINISHED}
question={question}
fullSql={detail?.sql}
/>
)}
</div>
);
};
const AnswerResultIterator = makeIterable(AnswerResultTemplate);
export default function PromptThread(props: Props) {
const { data } = props;
const divRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (divRef.current && data?.responses.length > 0) {
const contentLayout = divRef.current.parentElement;
const lastChild = divRef.current.lastElementChild as HTMLElement;
const lastChildDivider = lastChild.firstElementChild as HTMLElement;
if (
contentLayout.clientHeight <
lastChild.offsetTop + lastChild.clientHeight
) {
contentLayout.scrollTo({
top: lastChildDivider.offsetTop,
behavior: 'smooth',
});
}
}
}, [divRef, data]);
return (
<StyledPromptThread className="mt-12" ref={divRef}>
<AnswerResultIterator data={data?.responses || []} sql={data?.sql} />
</StyledPromptThread>
);
}
+12 -16
View File
@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import { useParams } from 'next/navigation';
import { Button } from 'antd';
import styled from 'styled-components';
import { DataNode } from 'antd/es/tree';
@@ -9,7 +10,6 @@ import PlusOutlined from '@ant-design/icons/PlusOutlined';
import SidebarTree, { useSidebarTreeState } from './SidebarTree';
import TreeTitle from '@/components/sidebar/home/TreeTitle';
// TODO: update it to real thread data type
interface ThreadData {
id: string;
name: string;
@@ -18,6 +18,8 @@ interface ThreadData {
export interface Props {
data: ThreadData[];
onSelect: (selectKeys) => void;
onDelete: (id: string) => Promise<void>;
onRename: (id: string, newName: string) => Promise<void>;
}
const StyledSidebarTree = styled(SidebarTree)`
@@ -39,8 +41,9 @@ const StyledSidebarTree = styled(SidebarTree)`
`;
export default function Home(props: Props) {
const { data, onSelect } = props;
const { data, onSelect, onRename, onDelete } = props;
const router = useRouter();
const params = useParams<{ id: string }>();
const getThreadGroupNode = createTreeGroupNode({
groupName: 'Thread',
@@ -52,8 +55,8 @@ export default function Home(props: Props) {
const { treeSelectedKeys, setTreeSelectedKeys } = useSidebarTreeState();
useEffect(() => {
router.query.id && setTreeSelectedKeys([router.query.id] as string[]);
}, [router.query.id]);
params?.id && setTreeSelectedKeys([params.id] as string[]);
}, [params?.id]);
// initial workspace
useEffect(() => {
@@ -70,25 +73,18 @@ export default function Home(props: Props) {
<TreeTitle
threadId={nodeKey}
title={thread.name}
onRename={(newThreadName) => {
// TODO: Call API to rename the thread name
console.log(
'Call API to rename the thread name:',
newThreadName,
);
}}
onRename={onRename}
onDelete={onDeleteThread}
/>
),
};
}),
);
}, [data]);
}, [params?.id, data]);
const onDeleteThread = (threadId: string) => {
// TODO: Call API to delete the thread result
console.log('Call delete API:', threadId);
if (router.query.id === threadId) {
const onDeleteThread = async (threadId: string) => {
await onDelete(threadId);
if (params?.id == threadId) {
router.push(Path.Home);
}
};
@@ -21,7 +21,7 @@ const StyledMenu = styled(Menu)`
export default function TreeTitle(props: {
threadId: string;
onDelete: (threadId: string) => void;
onRename: (newName: string) => void;
onRename: (threadId: string, newName: string) => void;
title: string;
}) {
const { threadId, onDelete, onRename } = props;
@@ -36,7 +36,7 @@ export default function TreeTitle(props: {
const onChangeTitle = (newThreadTitle: string) => {
setIsEditing(false);
setTitle(newThreadTitle);
onRename(newThreadTitle);
onRename(threadId, newThreadTitle);
};
return isEditing ? (
+7 -1
View File
@@ -1,6 +1,13 @@
import { useState } from 'react';
import { PROCESS_STATE } from '@/utils/enum';
export const getIsProcessing = (status: PROCESS_STATE) =>
[
PROCESS_STATE.UNDERSTANDING,
PROCESS_STATE.GENERATING,
PROCESS_STATE.SEARCHING,
].includes(status);
export default function useAskProcessState() {
const [currentState, setCurrentState] = useState<PROCESS_STATE>(
PROCESS_STATE.IDLE,
@@ -12,7 +19,6 @@ export default function useAskProcessState() {
const nextState = () => {
setCurrentState(currentState + 1);
console.log(currentState);
};
const setState = (state: PROCESS_STATE) => {
+60
View File
@@ -0,0 +1,60 @@
import { useEffect, useMemo } from 'react';
import { AskingTaskStatus } from '@/apollo/client/graphql/__types__';
import {
useAskingTaskLazyQuery,
useCancelAskingTaskMutation,
useCreateAskingTaskMutation,
} from '@/apollo/client/graphql/home.generated';
export const getIsFinished = (status: AskingTaskStatus) =>
[
AskingTaskStatus.FINISHED,
AskingTaskStatus.FAILED,
AskingTaskStatus.STOPPED,
].includes(status);
export default function useAskPrompt(threadId?: number) {
const [createAskingTask, createAskingTaskResult] =
useCreateAskingTaskMutation();
const [cancelAskingTask] = useCancelAskingTaskMutation();
const [fetchAskingTask, askingTaskResult] = useAskingTaskLazyQuery({
pollInterval: 1000,
});
const data = useMemo(
() => askingTaskResult.data?.askingTask || null,
[askingTaskResult.data],
);
const isFinished = useMemo(() => getIsFinished(data?.status), [data]);
useEffect(() => {
if (isFinished) askingTaskResult.stopPolling();
}, [isFinished]);
const onStop = () => {
const taskId = createAskingTaskResult.data?.createAskingTask.id;
if (taskId) {
cancelAskingTask({ variables: { taskId } }).catch((error) =>
console.error(error),
);
}
};
const onSubmit = async (value) => {
try {
const response = await createAskingTask({
variables: { data: { question: value, threadId } },
});
await fetchAskingTask({
variables: { taskId: response.data.createAskingTask.id },
});
} catch (error) {
console.error(error);
}
};
return {
data,
onStop,
onSubmit,
};
}
+33 -6
View File
@@ -1,21 +1,48 @@
import { useMemo } from 'react';
import { useRouter } from 'next/router';
import { Path } from '@/utils/enum';
import {
useDeleteThreadMutation,
useThreadsQuery,
useUpdateThreadMutation,
} from '@/apollo/client/graphql/home.generated';
export default function useHomeSidebar() {
const router = useRouter();
const { data, refetch } = useThreadsQuery();
const [updateThread] = useUpdateThreadMutation();
const [deleteThread] = useDeleteThreadMutation();
// TODO: call API to get real thread list data
const data = [
{ name: 'aaaa', id: 'aaa' },
{ name: 'bbbb', id: 'bbb' },
];
const threads = useMemo(
() =>
(data?.threads || []).map((thread) => ({
id: thread.id.toString(),
name: thread.summary,
})),
[data],
);
const onSelect = (selectKeys: string[]) => {
router.push(`${Path.Home}/${selectKeys[0]}`);
};
const onRename = async (id: string, newName: string) => {
await updateThread({
variables: { where: { id: Number(id) }, data: { summary: newName } },
});
refetch();
};
const onDelete = async (id) => {
await deleteThread({ variables: { where: { id: Number(id) } } });
refetch();
};
return {
data,
data: threads,
onSelect,
onRename,
onDelete,
refetch,
};
}
+101 -135
View File
@@ -1,152 +1,118 @@
import { useEffect, useRef, useState } from 'react';
import { Divider } from 'antd';
import styled from 'styled-components';
import { nextTick } from '@/utils/time';
import { GetServerSideProps } from 'next';
import { useRouter } from 'next/router';
import { useEffect, useMemo } from 'react';
import { Path } from '@/utils/enum';
import useHomeSidebar from '@/hooks/useHomeSidebar';
import useModalAction from '@/hooks/useModalAction';
import SiderLayout from '@/components/layouts/SiderLayout';
import AnswerResult from '@/components/pages/home/AnswerResult';
import SaveAsViewModal from '@/components/modals/SaveAsViewModal';
import Prompt from '@/components/pages/home/prompt';
import {
useCreateThreadResponseMutation,
useThreadQuery,
useThreadResponseLazyQuery,
} from '@/apollo/client/graphql/home.generated';
import useAskPrompt, { getIsFinished } from '@/hooks/useAskPrompt';
import PromptThread from '@/components/pages/home/promptThread';
const AnswerResultsBlock = styled.div`
width: 768px;
margin-left: auto;
margin-right: auto;
h4.ant-typography {
margin-top: 10px;
}
.ant-typography pre {
border: none;
border-radius: 4px;
}
.ace_editor {
border: none;
}
button {
vertical-align: middle;
}
`;
const testData = {
status: '',
result: [
{
summary: 'Top 10 customer with most order from global customer in 2024',
sql: 'SELECT * FROM customer',
},
{
summary: 'Top 10 customer with most order from global customer in 2024',
sql: 'SELECT * FROM customer',
},
{
summary: 'Top 10 customer with most order from global customer in 2024',
sql: 'SELECT * FROM customer',
},
],
};
export default function AnswerBlock() {
const divRef = useRef<HTMLDivElement>(null);
export default function HomeThread({ threadId }) {
const router = useRouter();
const homeSidebar = useHomeSidebar();
const askPrompt = useAskPrompt(threadId);
const saveAsViewModal = useModalAction();
// TODO: adjust when intergrating with API
const [simulateData, setSimulateData] = useState(testData);
// TODO: implement scroll when has new answer result
useEffect(() => {
if (divRef.current) {
const contentLayout = divRef.current.parentElement;
const lastChild = divRef.current.lastElementChild as HTMLElement;
const lastChildDivider = lastChild.firstElementChild as HTMLElement;
if (
contentLayout.clientHeight <
lastChild.offsetTop + lastChild.clientHeight
) {
contentLayout.scrollTo({
top: lastChildDivider.offsetTop,
behavior: 'smooth',
});
}
}
}, [divRef]);
// TODO: call API to get real answer results
const answerResults = [
{
status: 'finished',
query: 'What is our MoM of sales revenue in 2023?',
summary: 'MoM of sales revenue in 2023',
sql: 'SELECT * FROM customer',
description:
'To calculate the Month-over-Month (MoM) growth rate of sales revenue in 2023. We can use `sales` model to calculate MoM.',
steps: [
{
summary:
'First, we calculate the total revenue for each month in 2023 with `sales` model.',
sql: `SELECT * FROM Revenue`,
},
{
summary:
"Then, we calculate the previous month's revenue for each month.",
sql: `WITH Revenue AS (\n SELECT \n custkey,\n orderstatus,\n sum(totalprice) as totalprice\n FROM Orders\n GROUP BY 1, 2\n)\nSELECT * FROM Revenue`,
},
{
summary:
"At last, we calculate the Month-over-Month growth rate as a percentage. This is done by subtracting the previous month's revenue from the current month's revenue, dividing by the previous month's revenue, and then multiplying by 100 to get a percentage.",
sql: 'SELECT *\nFROM tpch.sf1.lineitem\nlimit 200',
},
],
const {
data,
loading,
updateQuery: updateThreadQuery,
} = useThreadQuery({
variables: { threadId },
fetchPolicy: 'cache-and-network',
onError: () => router.push(Path.Home),
});
const [createThreadResponse] = useCreateThreadResponseMutation({
onCompleted(next) {
const nextResponse = next.createThreadResponse;
updateThreadQuery((prev) => {
return {
...prev,
thread: {
...prev.thread,
responses: [...prev.thread.responses, nextResponse],
},
};
});
},
];
});
const [fetchThreadResponse, threadResponseResult] =
useThreadResponseLazyQuery({
pollInterval: 1000,
onCompleted(next) {
const nextResponse = next.threadResponse;
updateThreadQuery((prev) => ({
...prev,
thread: {
...prev.thread,
responses: prev.thread.responses.map((response) =>
response.id === nextResponse.id ? nextResponse : response,
),
},
}));
},
});
const onStop = () => {
// TODO: send stop asking API
};
const thread = useMemo(() => data?.thread || null, [data]);
const threadResponse = useMemo(
() => threadResponseResult.data?.threadResponse || null,
[threadResponseResult.data],
);
const isFinished = useMemo(
() => getIsFinished(threadResponse?.status),
[threadResponse],
);
const simulateProcess = async () => {
setSimulateData({ ...simulateData, status: 'understanding' });
await nextTick(3000);
setSimulateData({ ...simulateData, status: 'searching' });
await nextTick(3000);
setSimulateData({ ...simulateData, status: 'finished' });
};
useEffect(() => {
const unfinishedRespose = (thread?.responses || []).find(
(response) => !getIsFinished(response.status),
);
const onSubmit = async (value) => {
console.log(value);
await simulateProcess();
if (unfinishedRespose) {
fetchThreadResponse({ variables: { responseId: unfinishedRespose.id } });
}
}, [thread]);
useEffect(() => {
if (isFinished) threadResponseResult.stopPolling();
}, [isFinished]);
const onSelect = async (payload) => {
try {
const response = await createThreadResponse({
variables: { threadId: thread.id, data: payload },
});
await fetchThreadResponse({
variables: { responseId: response.data.createThreadResponse.id },
});
} catch (error) {
console.error(error);
}
};
return (
<SiderLayout loading={false} sidebar={homeSidebar}>
<AnswerResultsBlock className="mt-12 mb-15" ref={divRef}>
{answerResults.map((answerResult, index) => (
<div key={`${answerResult.query}-${index}`}>
{index > 0 && <Divider />}
<AnswerResult
answerResultSteps={answerResult.steps}
description={answerResult.description}
loading={answerResult.status !== 'finished'}
onOpenSaveAsViewModal={saveAsViewModal.openModal}
question={answerResult.query}
sql={answerResult.sql}
/>
</div>
))}
</AnswerResultsBlock>
<SaveAsViewModal
{...saveAsViewModal.state}
onClose={saveAsViewModal.closeModal}
onSubmit={async (values) => {
console.log('save as view', values);
}}
<SiderLayout loading={loading} sidebar={homeSidebar}>
<PromptThread data={thread} />
<div className="py-10" />
<Prompt
data={askPrompt.data}
onSubmit={askPrompt.onSubmit}
onStop={askPrompt.onStop}
onSelect={onSelect}
/>
<Prompt data={simulateData} onSubmit={onSubmit} onStop={onStop} />
</SiderLayout>
);
}
export const getServerSideProps = (async (context) => {
return {
props: {
threadId: Number(context.params.id),
},
};
}) as GetServerSideProps<{ threadId: number }>;
+22 -54
View File
@@ -1,40 +1,12 @@
import { useState } from 'react';
import { nextTick } from '@/utils/time';
import { useRouter } from 'next/router';
import Image from 'next/image';
import { Path } from '@/utils/enum';
import SiderLayout from '@/components/layouts/SiderLayout';
import Prompt from '@/components/pages/home/prompt';
import DemoPrompt from '@/components/pages/home/prompt/DemoPrompt';
import useHomeSidebar from '@/hooks/useHomeSidebar';
const testData = {
status: 'searching',
result: [
{
summary: 'Top 10 customer with most order from global customer in 2024',
sql: 'SELECT * FROM customer',
},
{
summary: 'Top 10 customer with most order from global customer in 2024',
sql: 'SELECT * FROM customer',
},
{
summary: 'Top 10 customer with most order from global customer in 2024',
sql: 'SELECT * FROM customer',
},
],
};
const errorData = undefined;
// {
// message: '',
// extensions: {
// code: '000',
// data: {
// status: 'finished',
// // message: `Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 10\n at Main.main(Main.java:4)`,
// },
// },
// };
import useAskPrompt from '@/hooks/useAskPrompt';
import { useCreateThreadMutation } from '@/apollo/client/graphql/home.generated';
const demoData = [
{
@@ -53,30 +25,26 @@ const demoData = [
},
];
export default function Ask() {
export default function Home() {
const router = useRouter();
const homeSidebar = useHomeSidebar();
const askPrompt = useAskPrompt();
const [createThread] = useCreateThreadMutation({
onCompleted: () => homeSidebar.refetch(),
});
// TODO: adjust when intergrating with API
const [simulateData, setSimulateData] = useState(testData);
const isDemo = true;
const onDemoSelect = () => {};
const onStop = () => {
// TODO: send stop asking API
};
const simulateProcess = async () => {
setSimulateData({ ...simulateData, status: 'understanding' });
await nextTick(3000);
setSimulateData({ ...simulateData, status: 'searching' });
await nextTick(3000);
setSimulateData({ ...simulateData, status: 'finished' });
};
const onSubmit = async (value) => {
console.log(value);
await simulateProcess();
const onSelect = async (payload) => {
try {
const response = await createThread({ variables: { data: payload } });
router.push(Path.Home + `/${response.data.createThread.id}`);
} catch (error) {
console.error(error);
}
};
return (
@@ -99,10 +67,10 @@ export default function Ask() {
{isDemo && <DemoPrompt demo={demoData} onSelect={onDemoSelect} />}
</div>
<Prompt
data={simulateData}
error={errorData?.extensions.data}
onSubmit={onSubmit}
onStop={onStop}
data={askPrompt.data}
onSubmit={askPrompt.onSubmit}
onStop={askPrompt.onStop}
onSelect={onSelect}
/>
</SiderLayout>
);
+1 -2
View File
@@ -11,7 +11,6 @@ export enum PROCESS_STATE {
GENERATING,
FINISHED,
UNDERSTANDING_FAILED,
SEARCHING_FAILED,
FAILED,
NO_RESULT,
}
+53
View File
@@ -31,8 +31,61 @@ class SaveRelationsErrorHandler extends ErrorHandler {
}
}
class CreateAskingTaskErrorHandler extends ErrorHandler {
public getErrorMessage(error: GraphQLError) {
switch (error.extensions?.code) {
default:
return 'Failed to create asking task.';
}
}
}
class CreateThreadErrorHandler extends ErrorHandler {
public getErrorMessage(error: GraphQLError) {
switch (error.extensions?.code) {
default:
return 'Failed to create thread.';
}
}
}
class UpdateThreadErrorHandler extends ErrorHandler {
public getErrorMessage(error: GraphQLError) {
switch (error.extensions?.code) {
default:
return 'Failed to update thread.';
}
}
}
class DeleteThreadErrorHandler extends ErrorHandler {
public getErrorMessage(error: GraphQLError) {
switch (error.extensions?.code) {
default:
return 'Failed to delete thread.';
}
}
}
class CreateThreadResponseErrorHandler extends ErrorHandler {
public getErrorMessage(error: GraphQLError) {
switch (error.extensions?.code) {
default:
return 'Failed to create thread response.';
}
}
}
errorHandlers.set('SaveTables', new SaveTablesErrorHandler());
errorHandlers.set('SaveRelations', new SaveRelationsErrorHandler());
errorHandlers.set('CreateAskingTask', new CreateAskingTaskErrorHandler());
errorHandlers.set('CreateThread', new CreateThreadErrorHandler());
errorHandlers.set('UpdateThread', new UpdateThreadErrorHandler());
errorHandlers.set('DeleteThread', new DeleteThreadErrorHandler());
errorHandlers.set(
'CreateThreadResponse',
new CreateThreadResponseErrorHandler(),
);
const errorHandler = (error: ErrorResponse) => {
const operationName = error?.operation?.operationName || '';