Skip to content

Commit

Permalink
Merge pull request #5553 from gooddata/ach/gen-ai-feedback
Browse files Browse the repository at this point in the history
RELATED: F1-645 user feedback in chat
  • Loading branch information
andriichumak authored Nov 13, 2024
2 parents b01d375 + df88c2a commit c28cf28
Show file tree
Hide file tree
Showing 19 changed files with 268 additions and 10 deletions.
3 changes: 3 additions & 0 deletions libs/sdk-backend-base/api/sdk-backend-base.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { ErrorConverter } from '@gooddata/sdk-backend-spi';
import { ExplainConfig } from '@gooddata/sdk-backend-spi';
import { ExplainType } from '@gooddata/sdk-backend-spi';
import { FilterContextItem } from '@gooddata/sdk-model';
import { GenAIChatInteractionUserFeedback } from '@gooddata/sdk-model';
import { GenAIObjectType } from '@gooddata/sdk-model';
import { IAnalyticalBackend } from '@gooddata/sdk-backend-spi';
import { IAnalyticalBackendConfig } from '@gooddata/sdk-backend-spi';
Expand Down Expand Up @@ -670,6 +671,8 @@ export class DummyGenAIChatThread implements IChatThread {
query(_userMessage: string): IChatThreadQuery;
// (undocumented)
reset(): Promise<void>;
// (undocumented)
saveUserFeedback(_interactionId: number, _feedback: GenAIChatInteractionUserFeedback): Promise<void>;
}

// @internal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
IChatThreadQuery,
IGenAIChatEvaluation,
} from "@gooddata/sdk-backend-spi";
import { GenAIChatInteractionUserFeedback } from "@gooddata/sdk-model";

/**
* Dummy chat thread interface for testing.
Expand All @@ -23,6 +24,10 @@ export class DummyGenAIChatThread implements IChatThread {
async reset(): Promise<void> {
await cancellableTimeout(100);
}
async saveUserFeedback(
_interactionId: number,
_feedback: GenAIChatInteractionUserFeedback,
): Promise<void> {}
query(_userMessage: string): IChatThreadQuery {
return new DummyGenAIChatQueryBuilder();
}
Expand Down
2 changes: 2 additions & 0 deletions libs/sdk-backend-spi/api/sdk-backend-spi.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { CatalogItemType } from '@gooddata/sdk-model';
import { DataValue } from '@gooddata/sdk-model';
import { DimensionGenerator } from '@gooddata/sdk-model';
import { FilterContextItem } from '@gooddata/sdk-model';
import { GenAIChatInteractionUserFeedback } from '@gooddata/sdk-model';
import { GenAIObjectType } from '@gooddata/sdk-model';
import { IAbsoluteDateFilter } from '@gooddata/sdk-model';
import { IAccessGrantee } from '@gooddata/sdk-model';
Expand Down Expand Up @@ -409,6 +410,7 @@ export interface IChatThread {
}): Promise<IChatThreadHistory>;
query(userMessage: string): IChatThreadQuery;
reset(): Promise<void>;
saveUserFeedback(interactionId: number, feedback: GenAIChatInteractionUserFeedback): Promise<void>;
}

// @beta
Expand Down
5 changes: 5 additions & 0 deletions libs/sdk-backend-spi/src/workspace/genAI/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
IGenAIChatRouting,
IGenAIFoundObjects,
IGenAICreatedVisualizations,
GenAIChatInteractionUserFeedback,
} from "@gooddata/sdk-model";

/**
Expand Down Expand Up @@ -85,6 +86,10 @@ export interface IChatThread {
* Reset the chat thread history.
*/
reset(): Promise<void>;
/**
* Save user feedback for the interaction.
*/
saveUserFeedback(interactionId: number, feedback: GenAIChatInteractionUserFeedback): Promise<void>;
/**
* Add a user message to the chat thread.
*/
Expand Down
17 changes: 16 additions & 1 deletion libs/sdk-backend-tiger/src/backend/workspace/genAI/ChatThread.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// (C) 2024 GoodData Corporation

import { IGenAIUserContext } from "@gooddata/sdk-model";
import { GenAIChatInteractionUserFeedback, IGenAIUserContext } from "@gooddata/sdk-model";
import {
IChatThread,
IChatThreadHistory,
Expand Down Expand Up @@ -50,6 +50,21 @@ export class ChatThreadService implements IChatThread {
});
}

async saveUserFeedback(
chatHistoryInteractionId: number,
userFeedback: GenAIChatInteractionUserFeedback,
): Promise<void> {
await this.authCall((client) => {
return client.genAI.aiChatHistory({
workspaceId: this.workspaceId,
chatHistoryRequest: {
chatHistoryInteractionId,
userFeedback,
},
});
});
}

query(userMessage: string): IChatThreadQuery {
return new ChatThreadQuery(this.authCall, {
workspaceId: this.workspaceId,
Expand Down
2 changes: 2 additions & 0 deletions libs/sdk-ui-gen-ai/api/sdk-ui-gen-ai.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
```ts

import { GenAIChatInteractionUserFeedback } from '@gooddata/sdk-model';
import { GenAIChatRoutingUseCase } from '@gooddata/sdk-model';
import { IAnalyticalBackend } from '@gooddata/sdk-backend-spi';
import { IGenAIVisualization } from '@gooddata/sdk-model';
Expand All @@ -13,6 +14,7 @@ import { default as React_2 } from 'react';
// @alpha (undocumented)
export type AssistantMessage = BaseMessage & {
role: "assistant";
feedback: GenAIChatInteractionUserFeedback;
};

// @alpha (undocumented)
Expand Down
62 changes: 55 additions & 7 deletions libs/sdk-ui-gen-ai/src/components/messages/AssistantMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,20 @@ import { AssistantMessage, isErrorContents } from "../../model.js";
import { AgentIcon } from "./AgentIcon.js";
import { MessageContents } from "./MessageContents.js";
import { defineMessage, injectIntl, WrappedComponentProps } from "react-intl";
import { Icon } from "@gooddata/sdk-ui-kit";
import { connect } from "react-redux";
import { setUserFeedback } from "../../store/index.js";

type AssistantMessageProps = {
message: AssistantMessage;
setUserFeedback: typeof setUserFeedback;
};

const labelMessage = defineMessage({ id: "gd.gen-ai.assistant-icon" });

const AssistantMessageComponentCore: React.FC<AssistantMessageProps & WrappedComponentProps> = ({
message,
setUserFeedback,
intl,
}) => {
const classNames = cx(
Expand All @@ -32,14 +37,57 @@ const AssistantMessageComponentCore: React.FC<AssistantMessageProps & WrappedCom
cancelled={message.cancelled}
aria-label={intl.formatMessage(labelMessage)}
/>
<MessageContents
useMarkdown
content={message.content}
isComplete={Boolean(message.complete || message.cancelled)}
isCancelled={message.cancelled}
/>
<div className="gd-gen-ai-chat__messages__message__contents_wrap">
<MessageContents
useMarkdown
content={message.content}
isComplete={Boolean(message.complete || message.cancelled)}
isCancelled={message.cancelled}
/>
<div className="gd-gen-ai-chat__messages__feedback">
<button
className={cx({
"gd-gen-ai-chat__messages__feedback__button": true,
"gd-gen-ai-chat__messages__feedback__button--positive":
message.feedback === "POSITIVE",
})}
type="button"
onClick={() =>
setUserFeedback({
assistantMessageId: message.localId,
feedback: message.feedback === "POSITIVE" ? "NONE" : "POSITIVE",
})
}
>
<Icon.ThumbsUp />
</button>
<button
className={cx({
"gd-gen-ai-chat__messages__feedback__button": true,
"gd-gen-ai-chat__messages__feedback__button--negative":
message.feedback === "NEGATIVE",
})}
type="button"
onClick={() =>
setUserFeedback({
assistantMessageId: message.localId,
feedback: message.feedback === "NEGATIVE" ? "NONE" : "NEGATIVE",
})
}
>
<Icon.ThumbsDown />
</button>
</div>
</div>
</div>
);
};

export const AssistantMessageComponent = injectIntl(AssistantMessageComponentCore);
const mapDispatchToProps = {
setUserFeedback,
};

export const AssistantMessageComponent = connect(
null,
mapDispatchToProps,
)(injectIntl(AssistantMessageComponentCore));
9 changes: 8 additions & 1 deletion libs/sdk-ui-gen-ai/src/model.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
// (C) 2024 GoodData Corporation
import { v4 as uuidv4 } from "uuid";
import { GenAIChatRoutingUseCase, IGenAIVisualization, ISemanticSearchResultItem } from "@gooddata/sdk-model";
import {
GenAIChatRoutingUseCase,
GenAIChatInteractionUserFeedback,
IGenAIVisualization,
ISemanticSearchResultItem,
} from "@gooddata/sdk-model";

/**
* @alpha
Expand Down Expand Up @@ -191,6 +196,7 @@ export const makeUserMessage = (content: Contents[]): UserMessage => ({
*/
export type AssistantMessage = BaseMessage & {
role: "assistant";
feedback: GenAIChatInteractionUserFeedback;
};

/**
Expand All @@ -211,6 +217,7 @@ export const makeAssistantMessage = (
id: id,
localId: uuidv4(),
role: "assistant",
feedback: "NONE",
created: Date.now(),
cancelled: false,
complete,
Expand Down
1 change: 1 addition & 0 deletions libs/sdk-ui-gen-ai/src/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export {
setVerboseAction,
setGlobalErrorAction,
cancelAsyncAction,
setUserFeedback,
} from "./messages/messagesSlice.js";

export {
Expand Down
15 changes: 15 additions & 0 deletions libs/sdk-ui-gen-ai/src/store/messages/messagesSlice.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// (C) 2024 GoodData Corporation
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { GenAIChatInteractionUserFeedback } from "@gooddata/sdk-model";
import { AssistantMessage, Contents, isAssistantMessage, makeErrorContents, Message } from "../../model.js";

type MessagesSliceState = {
Expand Down Expand Up @@ -171,6 +172,19 @@ const messagesSlice = createSlice({
cancelAsyncAction: (state) => {
delete state.asyncProcess;
},
setUserFeedback: (
state,
{
payload,
}: PayloadAction<{
assistantMessageId: string;
feedback: GenAIChatInteractionUserFeedback;
}>,
) => {
const assistantMessage = getAssistantMessageStrict(state, payload.assistantMessageId);

assistantMessage.feedback = payload.feedback;
},
},
});

Expand All @@ -191,4 +205,5 @@ export const {
setVerboseAction,
setGlobalErrorAction,
cancelAsyncAction,
setUserFeedback,
} = messagesSlice.actions;
3 changes: 3 additions & 0 deletions libs/sdk-ui-gen-ai/src/store/sideEffects/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import {
loadThreadAction,
clearThreadAction,
newMessageAction,
setUserFeedback,
} from "../messages/messagesSlice.js";
import { onVerboseStore } from "./onVerboseStore.js";
import { onThreadLoad } from "./onThreadLoad.js";
import { onThreadClear } from "./onThreadClear.js";
import { onUserMessage } from "./onUserMessage.js";
import { onUserFeedback } from "./onUserFeedback.js";

/**
* One saga to rule them all.
Expand All @@ -21,4 +23,5 @@ export function* rootSaga() {
yield takeLatest(loadThreadAction.type, onThreadLoad);
yield takeLatest(clearThreadAction.type, onThreadClear);
yield takeLatest(newMessageAction.type, onUserMessage);
yield takeEvery(setUserFeedback.type, onUserFeedback);
}
37 changes: 37 additions & 0 deletions libs/sdk-ui-gen-ai/src/store/sideEffects/onUserFeedback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// (C) 2024 GoodData Corporation

import { IAnalyticalBackend } from "@gooddata/sdk-backend-spi";
import { PayloadAction } from "@reduxjs/toolkit";
import { call, getContext, select } from "redux-saga/effects";
import { extractError } from "./utils.js";
import { GenAIChatInteractionUserFeedback } from "@gooddata/sdk-model";
import { messagesSelector } from "../messages/messagesSelectors.js";
import { Message } from "../../model.js";

/**
* Save user feedback to server.
* Optimistic update, ignoring the error.
* @internal
*/
export function* onUserFeedback({
payload,
}: PayloadAction<{
assistantMessageId: string;
feedback: GenAIChatInteractionUserFeedback;
}>) {
try {
// Retrieve backend from context
const backend: IAnalyticalBackend = yield getContext("backend");
const workspace: string = yield getContext("workspace");
const messages: Message[] = yield select(messagesSelector);
const message = messages.find((message) => message.localId === payload.assistantMessageId);

if (!message?.id) return;

const chatThread = backend.workspace(workspace).genAI().getChatThread();

yield call([chatThread, chatThread.saveUserFeedback], message.id, payload.feedback);
} catch (e) {
console.warn(`Failed to save user feedback: ${extractError(e)}`);
}
}
51 changes: 50 additions & 1 deletion libs/sdk-ui-gen-ai/styles/scss/messages.scss
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,55 @@
display: flex;
}

.gd-gen-ai-chat__messages__message__contents_wrap {
display: flex;
flex-direction: column;
gap: 5px;
}

.gd-gen-ai-chat__messages__feedback {
display: flex;
flex-direction: row;
gap: 10px;
visibility: hidden;
opacity: 0;
height: 20px;
}

.gd-gen-ai-chat__messages__message:last-child .gd-gen-ai-chat__messages__feedback,
.gd-gen-ai-chat__messages__message:hover .gd-gen-ai-chat__messages__feedback {
visibility: visible;
opacity: 1;
transition: opacity 0.2s;
}

.gd-gen-ai-chat__messages__feedback__button {
background: none;
border: none;
outline: none;
cursor: pointer;

& path {
opacity: 0.8;
transition: opacity 0.2s, fill 0.2s;
}

&:hover path {
fill: kit-variables.$gd-palette-primary-base;
opacity: 1;
}

&--positive path,
&--positive:hover path {
fill: kit-variables.$gd-palette-success-base;
}

&--negative path,
&--negative:hover path {
fill: kit-variables.$gd-palette-error-base;
}
}

.gd-gen-ai-chat__messages__message--user {
.gd-gen-ai-chat__messages__content--text {
$tail-height: 7px;
Expand Down Expand Up @@ -71,7 +120,7 @@
flex-direction: row;
gap: 10px;
align-items: flex-start;
margin-bottom: 39px;
margin-bottom: 19px;
}

.gd-gen-ai-chat__messages__contents {
Expand Down
Loading

0 comments on commit c28cf28

Please sign in to comment.