From d699f5ddfa0b94efeee98c7016318d472b2e3b2b Mon Sep 17 00:00:00 2001 From: Ajay Lamba Date: Sun, 24 Dec 2023 18:48:56 +0530 Subject: [PATCH 1/3] Enhancement: Add user feedback for responses Added basic feedback mechanism for responses generated by the chatbot. The feedbacks are stored in DynamoDB which can be queried to do analysis as required by admin users. In future we can add a UI page to display the feedbacks, but for now these are being stored and manual analysis would be required. The feedbacks are not adding to the learning of the chatbot. --- .../chatbot-dynamodb-tables/index.ts | 22 +++++ .../functions/api-handler/index.py | 2 + .../api-handler/routes/user_feedback.py | 35 +++++++ lib/chatbot-api/index.ts | 6 ++ lib/chatbot-api/rest-api.ts | 5 + .../python/genai_core/user_feedback.py | 36 ++++++++ .../react-app/package-lock.json | 92 +++++++++++++++++++ lib/user-interface/react-app/package.json | 4 + .../src/common/api-client/api-client.ts | 10 ++ .../common/api-client/user-feedback-client.ts | 29 ++++++ .../src/components/chatbot/chat-message.tsx | 24 +++++ .../react-app/src/components/chatbot/chat.tsx | 15 +++ .../src/components/chatbot/multi-chat.tsx | 15 +++ .../react-app/src/styles/chat.module.scss | 34 +++++++ 14 files changed, 329 insertions(+) create mode 100644 lib/chatbot-api/functions/api-handler/routes/user_feedback.py create mode 100644 lib/shared/layers/python-sdk/python/genai_core/user_feedback.py create mode 100644 lib/user-interface/react-app/src/common/api-client/user-feedback-client.ts diff --git a/lib/chatbot-api/chatbot-dynamodb-tables/index.ts b/lib/chatbot-api/chatbot-dynamodb-tables/index.ts index 28f6cefb5..ca1e9b31a 100644 --- a/lib/chatbot-api/chatbot-dynamodb-tables/index.ts +++ b/lib/chatbot-api/chatbot-dynamodb-tables/index.ts @@ -4,7 +4,9 @@ import * as dynamodb from "aws-cdk-lib/aws-dynamodb"; export class ChatBotDynamoDBTables extends Construct { public readonly sessionsTable: dynamodb.Table; + public readonly userFeedbackTable: dynamodb.Table; public readonly byUserIdIndex: string = "byUserId"; + public readonly bySessionIdIndex: string = "bySessionId"; constructor(scope: Construct, id: string) { super(scope, id); @@ -28,6 +30,26 @@ export class ChatBotDynamoDBTables extends Construct { partitionKey: { name: "UserId", type: dynamodb.AttributeType.STRING }, }); + const userFeedbackTable = new dynamodb.Table(this, "UserFeedbackTable", { + partitionKey: { + name: "FeedbackId", + type: dynamodb.AttributeType.STRING, + }, + sortKey: { + name: "SessionId", + type: dynamodb.AttributeType.STRING, + }, + billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, + encryption: dynamodb.TableEncryption.AWS_MANAGED, + removalPolicy: cdk.RemovalPolicy.DESTROY + }); + + userFeedbackTable.addGlobalSecondaryIndex({ + indexName: this.bySessionIdIndex, + partitionKey: { name: "SessionId", type: dynamodb.AttributeType.STRING} + }); + this.sessionsTable = sessionsTable; + this.userFeedbackTable = userFeedbackTable; } } diff --git a/lib/chatbot-api/functions/api-handler/index.py b/lib/chatbot-api/functions/api-handler/index.py index 6b255334f..a3a268cfa 100644 --- a/lib/chatbot-api/functions/api-handler/index.py +++ b/lib/chatbot-api/functions/api-handler/index.py @@ -23,6 +23,7 @@ from routes.semantic_search import router as semantic_search_router from routes.documents import router as documents_router from routes.kendra import router as kendra_router +from routes.user_feedback import router as user_feedback_router tracer = Tracer() logger = Logger() @@ -45,6 +46,7 @@ app.include_router(semantic_search_router) app.include_router(documents_router) app.include_router(kendra_router) +app.include_router(user_feedback_router) diff --git a/lib/chatbot-api/functions/api-handler/routes/user_feedback.py b/lib/chatbot-api/functions/api-handler/routes/user_feedback.py new file mode 100644 index 000000000..a2715c6a1 --- /dev/null +++ b/lib/chatbot-api/functions/api-handler/routes/user_feedback.py @@ -0,0 +1,35 @@ +import genai_core.types +import genai_core.auth +import genai_core.user_feedback +from pydantic import BaseModel +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler.api_gateway import Router + +tracer = Tracer() +router = Router() +logger = Logger() + + +class CreateUserFeedbackRequest(BaseModel): + sessionId: str + key: str + feedback: str + +@router.put("/user-feedback") +@tracer.capture_method +def user_feedback(): + data: dict = router.current_event.json_body + request = CreateUserFeedbackRequest(**data) + + session_id = request.sessionId + key = request.key + feedback = request.feedback + + user_id = genai_core.auth.get_user_id(router) + + if user_id is None: + raise genai_core.types.CommonError("User not found") + + result = genai_core.user_feedback.add_user_feedback(session_id, user_id, key, feedback) + + return {"ok": True, "data": result} diff --git a/lib/chatbot-api/index.ts b/lib/chatbot-api/index.ts index f1dda76ed..a7a6057c9 100644 --- a/lib/chatbot-api/index.ts +++ b/lib/chatbot-api/index.ts @@ -29,6 +29,8 @@ export class ChatBotApi extends Construct { public readonly messagesTopic: sns.Topic; public readonly sessionsTable: dynamodb.Table; public readonly byUserIdIndex: string; + public readonly userFeedbackTable: dynamodb.Table; + public readonly bySessionIdIndex: string; public readonly filesBucket: s3.Bucket; constructor(scope: Construct, id: string, props: ChatBotApiProps) { @@ -41,6 +43,8 @@ export class ChatBotApi extends Construct { ...props, sessionsTable: chatTables.sessionsTable, byUserIdIndex: chatTables.byUserIdIndex, + userFeedbackTable: chatTables.userFeedbackTable, + bySessionIdIndex: chatTables.bySessionIdIndex }); const webSocketApi = new WebSocketApi(this, "WebSocketApi", props); @@ -50,6 +54,8 @@ export class ChatBotApi extends Construct { this.messagesTopic = webSocketApi.messagesTopic; this.sessionsTable = chatTables.sessionsTable; this.byUserIdIndex = chatTables.byUserIdIndex; + this.userFeedbackTable = chatTables.userFeedbackTable; + this.bySessionIdIndex = chatTables.bySessionIdIndex; this.filesBucket = chatBuckets.filesBucket; } } diff --git a/lib/chatbot-api/rest-api.ts b/lib/chatbot-api/rest-api.ts index 9bc4b2744..851df5387 100644 --- a/lib/chatbot-api/rest-api.ts +++ b/lib/chatbot-api/rest-api.ts @@ -20,6 +20,8 @@ export interface RestApiProps { readonly userPool: cognito.UserPool; readonly sessionsTable: dynamodb.Table; readonly byUserIdIndex: string; + readonly userFeedbackTable: dynamodb.Table; + readonly bySessionIdIndex: string; readonly modelsParameter: ssm.StringParameter; readonly models: SageMakerModelEndpoint[]; } @@ -57,6 +59,8 @@ export class RestApi extends Construct { API_KEYS_SECRETS_ARN: props.shared.apiKeysSecret.secretArn, SESSIONS_TABLE_NAME: props.sessionsTable.tableName, SESSIONS_BY_USER_ID_INDEX_NAME: props.byUserIdIndex, + USER_FEEDBACK_TABLE_NAME: props.userFeedbackTable.tableName, + USER_FEEDBACK_BY_SESSION_ID_INDEX_NAME: props.bySessionIdIndex, UPLOAD_BUCKET_NAME: props.ragEngines?.uploadBucket?.bucketName ?? "", PROCESSING_BUCKET_NAME: props.ragEngines?.processingBucket?.bucketName ?? "", @@ -247,6 +251,7 @@ export class RestApi extends Construct { props.shared.configParameter.grantRead(apiHandler); props.modelsParameter.grantRead(apiHandler); props.sessionsTable.grantReadWriteData(apiHandler); + props.userFeedbackTable.grantReadWriteData(apiHandler); props.ragEngines?.uploadBucket.grantReadWrite(apiHandler); props.ragEngines?.processingBucket.grantReadWrite(apiHandler); diff --git a/lib/shared/layers/python-sdk/python/genai_core/user_feedback.py b/lib/shared/layers/python-sdk/python/genai_core/user_feedback.py new file mode 100644 index 000000000..26fa105b0 --- /dev/null +++ b/lib/shared/layers/python-sdk/python/genai_core/user_feedback.py @@ -0,0 +1,36 @@ +import os +import uuid +import boto3 +from datetime import datetime + +dynamodb = boto3.resource("dynamodb") + +USER_FEEDBACK_TABLE_NAME = os.environ.get("USER_FEEDBACK_TABLE_NAME") + +if USER_FEEDBACK_TABLE_NAME: + table = dynamodb.Table(USER_FEEDBACK_TABLE_NAME) + +def add_user_feedback( + session_id: str, + user_id: str, + key: str, + feedback: str +): + feedback_id = str(uuid.uuid4()) + timestamp = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%fZ") + + item = { + "FeedbackId": feedback_id, + "SessionId": session_id, + "UserId": user_id, + "Key": key, + "Feedback": feedback, + "CreatedAt": timestamp + } + + response = table.put_item(Item=item) + print(response) + + return { + "id": feedback_id + } diff --git a/lib/user-interface/react-app/package-lock.json b/lib/user-interface/react-app/package-lock.json index 9a763654a..70c467ef5 100644 --- a/lib/user-interface/react-app/package-lock.json +++ b/lib/user-interface/react-app/package-lock.json @@ -12,10 +12,14 @@ "@cloudscape-design/components": "^3.0.405", "@cloudscape-design/design-tokens": "^3.0.28", "@cloudscape-design/global-styles": "^1.0.13", + "@fortawesome/fontawesome-svg-core": "^6.4.2", + "@fortawesome/free-solid-svg-icons": "^6.4.2", + "@fortawesome/react-fontawesome": "^0.2.0", "aws-amplify": "^5.3.11", "luxon": "^3.4.3", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-icons": "^4.12.0", "react-json-view-lite": "^0.9.8", "react-markdown": "^9.0.0", "react-router-dom": "^6.15.0", @@ -8332,6 +8336,51 @@ "tslib": "^2.4.0" } }, + "node_modules/@fortawesome/fontawesome-common-types": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.1.tgz", + "integrity": "sha512-GkWzv+L6d2bI5f/Vk6ikJ9xtl7dfXtoRu3YGE6nq0p/FFqA1ebMOAWg3XgRyb0I6LYyYkiAo+3/KrwuBp8xG7A==", + "hasInstallScript": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-svg-core": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.1.tgz", + "integrity": "sha512-MfRCYlQPXoLlpem+egxjfkEuP9UQswTrlCOsknus/NcMoblTH2g0jPrapbcIb04KGA7E2GZxbAccGZfWoYgsrQ==", + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.5.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-solid-svg-icons": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.1.tgz", + "integrity": "sha512-S1PPfU3mIJa59biTtXJz1oI0+KAXW6bkAb31XKhxdxtuXDiUIFsih4JR1v5BbxY7hVHsD1RKq+jRkVRaf773NQ==", + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.5.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/react-fontawesome": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.0.tgz", + "integrity": "sha512-uHg75Rb/XORTtVt7OS9WoK8uM276Ufi7gCzshVWkUJbHhh3svsUUeqXerrM96Wm7fRiDzfKRwSoahhMIkGAYHw==", + "dependencies": { + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "@fortawesome/fontawesome-svg-core": "~1 || ~6", + "react": ">=16.3" + } + }, "node_modules/@hapi/hoek": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", @@ -16877,6 +16926,14 @@ "react": ">=16" } }, + "node_modules/react-icons": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.12.0.tgz", + "integrity": "sha512-IBaDuHiShdZqmfc/TwHu6+d6k2ltNCf3AszxNmjJc1KUfXdEeRJOKyNvLmAHaarhzGmTSVygNdyu8/opXv2gaw==", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -25893,6 +25950,35 @@ "tslib": "^2.4.0" } }, + "@fortawesome/fontawesome-common-types": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.1.tgz", + "integrity": "sha512-GkWzv+L6d2bI5f/Vk6ikJ9xtl7dfXtoRu3YGE6nq0p/FFqA1ebMOAWg3XgRyb0I6LYyYkiAo+3/KrwuBp8xG7A==" + }, + "@fortawesome/fontawesome-svg-core": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.1.tgz", + "integrity": "sha512-MfRCYlQPXoLlpem+egxjfkEuP9UQswTrlCOsknus/NcMoblTH2g0jPrapbcIb04KGA7E2GZxbAccGZfWoYgsrQ==", + "requires": { + "@fortawesome/fontawesome-common-types": "6.5.1" + } + }, + "@fortawesome/free-solid-svg-icons": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.1.tgz", + "integrity": "sha512-S1PPfU3mIJa59biTtXJz1oI0+KAXW6bkAb31XKhxdxtuXDiUIFsih4JR1v5BbxY7hVHsD1RKq+jRkVRaf773NQ==", + "requires": { + "@fortawesome/fontawesome-common-types": "6.5.1" + } + }, + "@fortawesome/react-fontawesome": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.0.tgz", + "integrity": "sha512-uHg75Rb/XORTtVt7OS9WoK8uM276Ufi7gCzshVWkUJbHhh3svsUUeqXerrM96Wm7fRiDzfKRwSoahhMIkGAYHw==", + "requires": { + "prop-types": "^15.8.1" + } + }, "@hapi/hoek": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", @@ -32241,6 +32327,12 @@ "integrity": "sha512-rOFGh3KgC2Ot66DmVCcT1p8lVJCmmCjzGE5WK/KsyDFi43wpIbW1PYcr04cQ3mbF4LaIkY6SpK7k1DOgwtpUXA==", "requires": {} }, + "react-icons": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.12.0.tgz", + "integrity": "sha512-IBaDuHiShdZqmfc/TwHu6+d6k2ltNCf3AszxNmjJc1KUfXdEeRJOKyNvLmAHaarhzGmTSVygNdyu8/opXv2gaw==", + "requires": {} + }, "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", diff --git a/lib/user-interface/react-app/package.json b/lib/user-interface/react-app/package.json index de5dfaf56..f876759d2 100644 --- a/lib/user-interface/react-app/package.json +++ b/lib/user-interface/react-app/package.json @@ -16,10 +16,14 @@ "@cloudscape-design/components": "^3.0.405", "@cloudscape-design/design-tokens": "^3.0.28", "@cloudscape-design/global-styles": "^1.0.13", + "@fortawesome/fontawesome-svg-core": "^6.4.2", + "@fortawesome/free-solid-svg-icons": "^6.4.2", + "@fortawesome/react-fontawesome": "^0.2.0", "aws-amplify": "^5.3.11", "luxon": "^3.4.3", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-icons": "^4.12.0", "react-json-view-lite": "^0.9.8", "react-markdown": "^9.0.0", "remark-gfm": "^4.0.0", diff --git a/lib/user-interface/react-app/src/common/api-client/api-client.ts b/lib/user-interface/react-app/src/common/api-client/api-client.ts index 26b91a709..a30983eec 100644 --- a/lib/user-interface/react-app/src/common/api-client/api-client.ts +++ b/lib/user-interface/react-app/src/common/api-client/api-client.ts @@ -9,6 +9,7 @@ import { SessionsClient } from "./sessions-client"; import { SemanticSearchClient } from "./semantic-search-client"; import { DocumentsClient } from "./documents-client"; import { KendraClient } from "./kendra-client"; +import { UserFeedbackClient } from "./user-feedback-client"; export class ApiClient { private _healthClient: HealthClient | undefined; @@ -21,6 +22,7 @@ export class ApiClient { private _semanticSearchClient: SemanticSearchClient | undefined; private _documentsClient: DocumentsClient | undefined; private _kendraClient: KendraClient | undefined; + private _userFeedbackClient: UserFeedbackClient | undefined; public get health() { if (!this._healthClient) { @@ -102,5 +104,13 @@ export class ApiClient { return this._kendraClient; } + public get userFeedback() { + if(!this._userFeedbackClient) { + this._userFeedbackClient = new UserFeedbackClient(this._appConfig); + } + + return this._userFeedbackClient; + } + constructor(protected _appConfig: AppConfig) {} } diff --git a/lib/user-interface/react-app/src/common/api-client/user-feedback-client.ts b/lib/user-interface/react-app/src/common/api-client/user-feedback-client.ts new file mode 100644 index 000000000..6cd23c9b6 --- /dev/null +++ b/lib/user-interface/react-app/src/common/api-client/user-feedback-client.ts @@ -0,0 +1,29 @@ +import { ApiResult } from "../types"; +import { ApiClientBase } from "./api-client-base"; + +export class UserFeedbackClient extends ApiClientBase { + + async addUserFeedback(params: { + sessionId: string; + key: number; + feedback: string; + }): Promise> { + try { + const headers = await this.getHeaders(); + const result = await fetch(this.getApiUrl("/user-feedback"), { + method: "PUT", + headers, + body: JSON.stringify({ + ...params + }), + }); + + if(!result.ok) { + console.log("Result: ", result); + } + return result.json(); + } catch (error) { + return this.error(error); + } + } +} diff --git a/lib/user-interface/react-app/src/components/chatbot/chat-message.tsx b/lib/user-interface/react-app/src/components/chatbot/chat-message.tsx index 4519c02ea..90f294476 100644 --- a/lib/user-interface/react-app/src/components/chatbot/chat-message.tsx +++ b/lib/user-interface/react-app/src/components/chatbot/chat-message.tsx @@ -27,11 +27,14 @@ import { getSignedUrl } from "./utils"; import "react-json-view-lite/dist/index.css"; import "../../styles/app.scss"; +import { FaThumbsUp, FaThumbsDown } from "react-icons/fa"; export interface ChatMessageProps { message: ChatBotHistoryItem; configuration?: ChatBotConfiguration; showMetadata?: boolean; + onThumbsUp: () => void; + onThumbsDown: () => void; } export default function ChatMessage(props: ChatMessageProps) { @@ -40,6 +43,7 @@ export default function ChatMessage(props: ChatMessageProps) { const [files, setFiles] = useState([] as ImageFile[]); const [documentIndex, setDocumentIndex] = useState("0"); const [promptIndex, setPromptIndex] = useState("0"); + const [selectedIcon, setSelectedIcon] = useState<'thumbsUp' | 'thumbsDown' | null>(null); useEffect(() => { const getSignedUrls = async () => { @@ -270,6 +274,26 @@ export default function ChatMessage(props: ChatMessageProps) { }, }} /> +
+ {(selectedIcon === 'thumbsUp' || selectedIcon === null) && ( + { + props.onThumbsUp(); + setSelectedIcon('thumbsUp'); + }} + /> + )} + {(selectedIcon === 'thumbsDown' || selectedIcon === null) && ( + { + props.onThumbsDown(); + setSelectedIcon('thumbsDown'); + }} + /> + )} +
)} {loading && ( diff --git a/lib/user-interface/react-app/src/components/chatbot/chat.tsx b/lib/user-interface/react-app/src/components/chatbot/chat.tsx index 4638ca002..b57dd89f1 100644 --- a/lib/user-interface/react-app/src/components/chatbot/chat.tsx +++ b/lib/user-interface/react-app/src/components/chatbot/chat.tsx @@ -64,6 +64,19 @@ export default function Chat(props: { sessionId?: string }) { })(); }, [appContext, props.sessionId]); + const handleFeedback = (feedbackType: 'thumbsUp' | 'thumbsDown', idx: number, message: ChatBotHistoryItem) => { + if (message.metadata.sessionId) { + addUserFeedback(message.metadata.sessionId as string, idx, feedbackType); + } + }; + + const addUserFeedback = async (sessionId: string, key: number, feedback: string) => { + if (!appContext) return; + + const apiClient = new ApiClient(appContext); + await apiClient.userFeedback.addUserFeedback({sessionId, key, feedback}); + }; + return (
@@ -72,6 +85,8 @@ export default function Chat(props: { sessionId?: string }) { key={idx} message={message} configuration={configuration} + onThumbsUp={() => handleFeedback('thumbsUp', idx, message)} + onThumbsDown={() => handleFeedback('thumbsDown', idx, message)} /> ))} diff --git a/lib/user-interface/react-app/src/components/chatbot/multi-chat.tsx b/lib/user-interface/react-app/src/components/chatbot/multi-chat.tsx index 4a2cb6e75..cbe1476d7 100644 --- a/lib/user-interface/react-app/src/components/chatbot/multi-chat.tsx +++ b/lib/user-interface/react-app/src/components/chatbot/multi-chat.tsx @@ -268,6 +268,19 @@ export default function MultiChat() { [ReadyState.UNINSTANTIATED]: "Uninstantiated", }[readyState]; + const handleFeedback = (feedbackType: 'thumbsUp' | 'thumbsDown', idx: number, message: ChatBotHistoryItem) => { + if (message.metadata.sessionId) { + addUserFeedback(message.metadata.sessionId as string, idx, feedbackType); + } + }; + + const addUserFeedback = async (sessionId: string, key: number, feedback: string) => { + if (!appContext) return; + + const apiClient = new ApiClient(appContext); + await apiClient.userFeedback.addUserFeedback({sessionId, key, feedback}); + }; + return (
@@ -418,6 +431,8 @@ export default function MultiChat() { message={message} configuration={chatSessions[idx].configuration} showMetadata={showMetadata} + onThumbsUp={() => handleFeedback('thumbsUp', idx, message)} + onThumbsDown={() => handleFeedback('thumbsDown', idx, message)} /> ))} diff --git a/lib/user-interface/react-app/src/styles/chat.module.scss b/lib/user-interface/react-app/src/styles/chat.module.scss index 8a5516239..83e757346 100644 --- a/lib/user-interface/react-app/src/styles/chat.module.scss +++ b/lib/user-interface/react-app/src/styles/chat.module.scss @@ -157,3 +157,37 @@ .markdownTable tr:nth-child(even) { background-color: awsui.$color-background-container-content; } + +.thumbsContainer { + display: flex; + align-items: center; + margin-top: 8px; +} + +.thumbsIcon { + cursor: pointer; + margin-right: 10px; + opacity: 0.5; +} + +/* Styles for thumbs up icon. Should be compatible with dark theme */ +.thumbsUp { + color: #539fe5; +} + +/* Styles for thumbs down icon. Should be compatible with dark theme */ +.thumbsDown { + color: #539fe5; +} + +/* Style for clicked state */ +.clicked { + opacity: 0.5; + pointer-events: none; /* Disable pointer events for a clicked icon */ +} + +/* Styles for selected icon */ +.thumbsIcon.selected { + opacity: 1 !important; + pointer-events: none; /* Disable pointer events for the selected icon */ +} \ No newline at end of file From cebefd8ffcf22cf5d28f3db3bc92a5b03b27ed80 Mon Sep 17 00:00:00 2001 From: Ajay Lamba Date: Sun, 11 Feb 2024 19:35:33 +0530 Subject: [PATCH 2/3] Earlier we were storing the user feedback in DynamoDB table with minimal information. This was good for beta phase but was not useful if we want to do analysis on it using other services like Athena/Glue etc. As part of this change, we have changed the mechanism and the feedback is being stored in S3 bucket now. We are storing the feedback in the following format: { "feedbackId": "", "sessionId": "", "userId": "", "key": "", "prompt": "", "completion": "", "model": "", "feedback": "thumbsUp/thumbsDown", "createdAt": "" } This feedback can be used by AWS Athena/Glue etc to do analysis and also to train the model if required. We are keeping one object per feedback in S3 'STANDARD_IA' class. --- .../chatbot-dynamodb-tables/index.ts | 22 -------- lib/chatbot-api/chatbot-s3-buckets/index.ts | 25 +++++++++ .../api-handler/routes/user_feedback.py | 32 ++++++------ lib/chatbot-api/index.ts | 9 ++-- lib/chatbot-api/rest-api.ts | 6 ++- lib/chatbot-api/schema/schema.graphql | 14 +++++ .../python/genai_core/user_feedback.py | 51 ++++++++++++------- .../common/api-client/user-feedback-client.ts | 50 +++++++++--------- .../react-app/src/components/chatbot/chat.tsx | 18 +++++-- .../src/components/chatbot/multi-chat.tsx | 27 +++++++--- .../react-app/src/components/chatbot/types.ts | 9 ++++ 11 files changed, 166 insertions(+), 97 deletions(-) diff --git a/lib/chatbot-api/chatbot-dynamodb-tables/index.ts b/lib/chatbot-api/chatbot-dynamodb-tables/index.ts index 774017c1a..7b4b7726a 100644 --- a/lib/chatbot-api/chatbot-dynamodb-tables/index.ts +++ b/lib/chatbot-api/chatbot-dynamodb-tables/index.ts @@ -4,9 +4,7 @@ import * as dynamodb from "aws-cdk-lib/aws-dynamodb"; export class ChatBotDynamoDBTables extends Construct { public readonly sessionsTable: dynamodb.Table; - public readonly userFeedbackTable: dynamodb.Table; public readonly byUserIdIndex: string = "byUserId"; - public readonly bySessionIdIndex: string = "bySessionId"; constructor(scope: Construct, id: string) { super(scope, id); @@ -31,26 +29,6 @@ export class ChatBotDynamoDBTables extends Construct { partitionKey: { name: "UserId", type: dynamodb.AttributeType.STRING }, }); - const userFeedbackTable = new dynamodb.Table(this, "UserFeedbackTable", { - partitionKey: { - name: "FeedbackId", - type: dynamodb.AttributeType.STRING, - }, - sortKey: { - name: "SessionId", - type: dynamodb.AttributeType.STRING, - }, - billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, - encryption: dynamodb.TableEncryption.AWS_MANAGED, - removalPolicy: cdk.RemovalPolicy.DESTROY - }); - - userFeedbackTable.addGlobalSecondaryIndex({ - indexName: this.bySessionIdIndex, - partitionKey: { name: "SessionId", type: dynamodb.AttributeType.STRING} - }); - this.sessionsTable = sessionsTable; - this.userFeedbackTable = userFeedbackTable; } } diff --git a/lib/chatbot-api/chatbot-s3-buckets/index.ts b/lib/chatbot-api/chatbot-s3-buckets/index.ts index 79ec464bb..662badf34 100644 --- a/lib/chatbot-api/chatbot-s3-buckets/index.ts +++ b/lib/chatbot-api/chatbot-s3-buckets/index.ts @@ -5,6 +5,7 @@ import { NagSuppressions } from "cdk-nag"; export class ChatBotS3Buckets extends Construct { public readonly filesBucket: s3.Bucket; + public readonly userFeedbackBucket: s3.Bucket; constructor(scope: Construct, id: string) { super(scope, id); @@ -39,7 +40,31 @@ export class ChatBotS3Buckets extends Construct { ], }); + const userFeedbackBucket = new s3.Bucket(this, "UserFeedbackBucket", { + blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, + removalPolicy: cdk.RemovalPolicy.DESTROY, + autoDeleteObjects: true, + transferAcceleration: true, + enforceSSL: true, + serverAccessLogsBucket: logsBucket, + cors: [ + { + allowedHeaders: ["*"], + allowedMethods: [ + s3.HttpMethods.PUT, + s3.HttpMethods.POST, + s3.HttpMethods.GET, + s3.HttpMethods.HEAD, + ], + allowedOrigins: ["*"], + exposedHeaders: ["ETag"], + maxAge: 3000, + }, + ], + }); + this.filesBucket = filesBucket; + this.userFeedbackBucket = userFeedbackBucket; /** * CDK NAG suppression diff --git a/lib/chatbot-api/functions/api-handler/routes/user_feedback.py b/lib/chatbot-api/functions/api-handler/routes/user_feedback.py index a2715c6a1..8098f8f1c 100644 --- a/lib/chatbot-api/functions/api-handler/routes/user_feedback.py +++ b/lib/chatbot-api/functions/api-handler/routes/user_feedback.py @@ -3,7 +3,7 @@ import genai_core.user_feedback from pydantic import BaseModel from aws_lambda_powertools import Logger, Tracer -from aws_lambda_powertools.event_handler.api_gateway import Router +from aws_lambda_powertools.event_handler.appsync import Router tracer = Tracer() router = Router() @@ -14,22 +14,24 @@ class CreateUserFeedbackRequest(BaseModel): sessionId: str key: str feedback: str + prompt: str + completion: str + model: str + -@router.put("/user-feedback") +@router.resolver(field_name="addUserFeedback") @tracer.capture_method -def user_feedback(): - data: dict = router.current_event.json_body - request = CreateUserFeedbackRequest(**data) +def user_feedback(input: dict): + request = CreateUserFeedbackRequest(**input) + + userId = genai_core.auth.get_user_id(router) - session_id = request.sessionId - key = request.key - feedback = request.feedback - - user_id = genai_core.auth.get_user_id(router) - - if user_id is None: + if userId is None: raise genai_core.types.CommonError("User not found") + + result = genai_core.user_feedback.add_user_feedback( + request.sessionId, request.key, request.feedback, request.prompt, request.completion, request.model, userId) - result = genai_core.user_feedback.add_user_feedback(session_id, user_id, key, feedback) - - return {"ok": True, "data": result} + return { + "feedback_id": result["feedback_id"], + } diff --git a/lib/chatbot-api/index.ts b/lib/chatbot-api/index.ts index aea5997d3..55406b424 100644 --- a/lib/chatbot-api/index.ts +++ b/lib/chatbot-api/index.ts @@ -31,9 +31,8 @@ export class ChatBotApi extends Construct { public readonly messagesTopic: sns.Topic; public readonly sessionsTable: dynamodb.Table; public readonly byUserIdIndex: string; - public readonly userFeedbackTable: dynamodb.Table; - public readonly bySessionIdIndex: string; public readonly filesBucket: s3.Bucket; + public readonly userFeedbackBucket: s3.Bucket; public readonly graphqlApi: appsync.GraphqlApi; constructor(scope: Construct, id: string, props: ChatBotApiProps) { @@ -89,8 +88,7 @@ export class ChatBotApi extends Construct { sessionsTable: chatTables.sessionsTable, byUserIdIndex: chatTables.byUserIdIndex, api, - userFeedbackTable: chatTables.userFeedbackTable, - bySessionIdIndex: chatTables.bySessionIdIndex + userFeedbackBucket: chatBuckets.userFeedbackBucket, }); const realtimeBackend = new RealtimeGraphqlApiBackend(this, "Realtime", { @@ -118,8 +116,7 @@ export class ChatBotApi extends Construct { this.messagesTopic = realtimeBackend.messagesTopic; this.sessionsTable = chatTables.sessionsTable; this.byUserIdIndex = chatTables.byUserIdIndex; - this.userFeedbackTable = chatTables.userFeedbackTable; - this.bySessionIdIndex = chatTables.bySessionIdIndex; + this.userFeedbackBucket = chatBuckets.userFeedbackBucket; this.filesBucket = chatBuckets.filesBucket; this.graphqlApi = api; diff --git a/lib/chatbot-api/rest-api.ts b/lib/chatbot-api/rest-api.ts index ac57abb85..a0ac62711 100644 --- a/lib/chatbot-api/rest-api.ts +++ b/lib/chatbot-api/rest-api.ts @@ -14,6 +14,7 @@ import { Shared } from "../shared"; import * as appsync from "aws-cdk-lib/aws-appsync"; import { parse } from "graphql"; import { readFileSync } from "fs"; +import * as s3 from "aws-cdk-lib/aws-s3"; export interface ApiResolversProps { readonly shared: Shared; @@ -22,8 +23,7 @@ export interface ApiResolversProps { readonly userPool: cognito.UserPool; readonly sessionsTable: dynamodb.Table; readonly byUserIdIndex: string; - readonly userFeedbackTable: dynamodb.Table; - readonly bySessionIdIndex: string; + readonly userFeedbackBucket: s3.Bucket; readonly modelsParameter: ssm.StringParameter; readonly models: SageMakerModelEndpoint[]; readonly api: appsync.GraphqlApi; @@ -64,6 +64,7 @@ export class ApiResolvers extends Construct { API_KEYS_SECRETS_ARN: props.shared.apiKeysSecret.secretArn, SESSIONS_TABLE_NAME: props.sessionsTable.tableName, SESSIONS_BY_USER_ID_INDEX_NAME: props.byUserIdIndex, + USER_FEEDBACK_BUCKET_NAME: props.userFeedbackBucket?.bucketName ?? "", UPLOAD_BUCKET_NAME: props.ragEngines?.uploadBucket?.bucketName ?? "", PROCESSING_BUCKET_NAME: props.ragEngines?.processingBucket?.bucketName ?? "", @@ -257,6 +258,7 @@ export class ApiResolvers extends Construct { props.shared.configParameter.grantRead(apiHandler); props.modelsParameter.grantRead(apiHandler); props.sessionsTable.grantReadWriteData(apiHandler); + props.userFeedbackBucket.grantReadWrite(apiHandler); props.ragEngines?.uploadBucket.grantReadWrite(apiHandler); props.ragEngines?.processingBucket.grantReadWrite(apiHandler); diff --git a/lib/chatbot-api/schema/schema.graphql b/lib/chatbot-api/schema/schema.graphql index bb10fd60d..b9d7eee35 100644 --- a/lib/chatbot-api/schema/schema.graphql +++ b/lib/chatbot-api/schema/schema.graphql @@ -89,6 +89,10 @@ type DocumentResult @aws_cognito_user_pools { status: String } +type UserFeedbackResult @aws_cognito_user_pools { + feedback_id: String! +} + input DocumentSubscriptionStatusInput { workspaceId: String! documentId: String! @@ -235,6 +239,15 @@ type SessionHistoryItem @aws_cognito_user_pools { metadata: String } +input UserFeedbackInput { + sessionId: String! + key: Int! + feedback: String! + prompt: String! + completion: String! + model: String! +} + input TextDocumentInput { workspaceId: String! title: String! @@ -296,6 +309,7 @@ type Mutation { deleteWorkspace(workspaceId: String!): Boolean @aws_cognito_user_pools addTextDocument(input: TextDocumentInput!): DocumentResult @aws_cognito_user_pools + addUserFeedback(input: UserFeedbackInput!): UserFeedbackResult @aws_cognito_user_pools addQnADocument(input: QnADocumentInput!): DocumentResult @aws_cognito_user_pools setDocumentSubscriptionStatus( diff --git a/lib/shared/layers/python-sdk/python/genai_core/user_feedback.py b/lib/shared/layers/python-sdk/python/genai_core/user_feedback.py index 26fa105b0..e0ce9f84c 100644 --- a/lib/shared/layers/python-sdk/python/genai_core/user_feedback.py +++ b/lib/shared/layers/python-sdk/python/genai_core/user_feedback.py @@ -1,36 +1,51 @@ import os import uuid import boto3 +import json +from pydantic import BaseModel from datetime import datetime dynamodb = boto3.resource("dynamodb") +s3_client = boto3.client("s3") -USER_FEEDBACK_TABLE_NAME = os.environ.get("USER_FEEDBACK_TABLE_NAME") +USER_FEEDBACK_BUCKET_NAME = os.environ.get("USER_FEEDBACK_BUCKET_NAME") -if USER_FEEDBACK_TABLE_NAME: - table = dynamodb.Table(USER_FEEDBACK_TABLE_NAME) def add_user_feedback( - session_id: str, - user_id: str, + sessionId: str, key: str, - feedback: str + feedback: str, + prompt: str, + completion: str, + model: str, + userId: str ): - feedback_id = str(uuid.uuid4()) + feedbackId = str(uuid.uuid4()) timestamp = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%fZ") - + item = { - "FeedbackId": feedback_id, - "SessionId": session_id, - "UserId": user_id, - "Key": key, - "Feedback": feedback, - "CreatedAt": timestamp + "feedbackId": feedbackId, + "sessionId": sessionId, + "userId": userId, + "key": key, + "prompt": prompt, + "completion": completion, + "model": model, + "feedback": feedback, + "createdAt": timestamp } - - response = table.put_item(Item=item) + + response = s3_client.put_object( + Bucket=USER_FEEDBACK_BUCKET_NAME, + Key=feedbackId, + Body=json.dumps(item), + ContentType="application/json", + StorageClass='STANDARD_IA', + ) print(response) - + return { - "id": feedback_id + "feedback_id": feedbackId } + + \ No newline at end of file diff --git a/lib/user-interface/react-app/src/common/api-client/user-feedback-client.ts b/lib/user-interface/react-app/src/common/api-client/user-feedback-client.ts index 6cd23c9b6..70452cb96 100644 --- a/lib/user-interface/react-app/src/common/api-client/user-feedback-client.ts +++ b/lib/user-interface/react-app/src/common/api-client/user-feedback-client.ts @@ -1,29 +1,31 @@ -import { ApiResult } from "../types"; -import { ApiClientBase } from "./api-client-base"; +import { GraphQLResult } from "@aws-amplify/api-graphql"; +import { API, GraphQLQuery } from "@aws-amplify/api"; +import { AddUserFeedbackMutation } from "../../API.ts"; +import { + addUserFeedback +} from "../../graphql/mutations.ts"; +import { FeedbackData } from "../../components/chatbot/types.ts"; -export class UserFeedbackClient extends ApiClientBase { +export class UserFeedbackClient { async addUserFeedback(params: { - sessionId: string; - key: number; - feedback: string; - }): Promise> { - try { - const headers = await this.getHeaders(); - const result = await fetch(this.getApiUrl("/user-feedback"), { - method: "PUT", - headers, - body: JSON.stringify({ - ...params - }), - }); - - if(!result.ok) { - console.log("Result: ", result); - } - return result.json(); - } catch (error) { - return this.error(error); - } + feedbackData: FeedbackData + } + ): Promise>> { + const result = API.graphql>({ + query: addUserFeedback, + variables: { + input: { + sessionId: params.feedbackData.sessionId, + key: params.feedbackData.key, + feedback: params.feedbackData.feedback, + prompt: params.feedbackData.prompt, + completion: params.feedbackData.completion, + model: params.feedbackData.model, + }, + }, + }); + return result; } + } diff --git a/lib/user-interface/react-app/src/components/chatbot/chat.tsx b/lib/user-interface/react-app/src/components/chatbot/chat.tsx index ecc29a967..e888da028 100644 --- a/lib/user-interface/react-app/src/components/chatbot/chat.tsx +++ b/lib/user-interface/react-app/src/components/chatbot/chat.tsx @@ -3,6 +3,7 @@ import { ChatBotConfiguration, ChatBotHistoryItem, ChatBotMessageType, + FeedbackData, } from "./types"; import { SpaceBetween, StatusIndicator } from "@cloudscape-design/components"; import { v4 as uuidv4 } from "uuid"; @@ -81,15 +82,26 @@ export default function Chat(props: { sessionId?: string }) { const handleFeedback = (feedbackType: 'thumbsUp' | 'thumbsDown', idx: number, message: ChatBotHistoryItem) => { if (message.metadata.sessionId) { - addUserFeedback(message.metadata.sessionId as string, idx, feedbackType); + const prompt = messageHistory[idx - 1]?.content; + const completion = message.content; + const model = message.metadata.modelId; + const feedbackData: FeedbackData = { + sessionId: message.metadata.sessionId as string, + key: idx, + feedback: feedbackType, + prompt: prompt, + completion: completion, + model: model as string + }; + addUserFeedback(feedbackData); } }; - const addUserFeedback = async (sessionId: string, key: number, feedback: string) => { + const addUserFeedback = async (feedbackData: FeedbackData) => { if (!appContext) return; const apiClient = new ApiClient(appContext); - await apiClient.userFeedback.addUserFeedback({sessionId, key, feedback}); + await apiClient.userFeedback.addUserFeedback({feedbackData}); }; return ( diff --git a/lib/user-interface/react-app/src/components/chatbot/multi-chat.tsx b/lib/user-interface/react-app/src/components/chatbot/multi-chat.tsx index b068cce7a..e6f604ecc 100644 --- a/lib/user-interface/react-app/src/components/chatbot/multi-chat.tsx +++ b/lib/user-interface/react-app/src/components/chatbot/multi-chat.tsx @@ -37,6 +37,7 @@ import { ChabotOutputModality, ChatBotHeartbeatRequest, ChatBotModelInterface, + FeedbackData, } from "./types"; import { LoadingStatus, ModelInterface } from "../../common/types"; import { getSelectedModelMetadata, updateMessageHistoryRef } from "./utils"; @@ -44,7 +45,7 @@ import LLMConfigDialog from "./llm-config-dialog"; import styles from "../../styles/chat.module.scss"; import { useNavigate } from "react-router-dom"; import { receiveMessages } from "../../graphql/subscriptions"; -import { sendQuery } from "../../graphql/mutations"; +import { sendQuery } from "../../graphql/mutations.ts"; import { Utils } from "../../common/utils"; export interface ChatSession { @@ -329,17 +330,29 @@ export default function MultiChat() { [ReadyState.UNINSTANTIATED]: "Uninstantiated", }[readyState]; - const handleFeedback = (feedbackType: 'thumbsUp' | 'thumbsDown', idx: number, message: ChatBotHistoryItem) => { + const handleFeedback = (feedbackType: 'thumbsUp' | 'thumbsDown', idx: number, message: ChatBotHistoryItem, messageHistory: ChatBotHistoryItem[]) => { + console.log("Message history: ", messageHistory); if (message.metadata.sessionId) { - addUserFeedback(message.metadata.sessionId as string, idx, feedbackType); + const prompt = messageHistory[idx - 1]?.content; + const completion = message.content; + const model = message.metadata.modelId; + const feedbackData: FeedbackData = { + sessionId: message.metadata.sessionId as string, + key: idx, + feedback: feedbackType, + prompt: prompt, + completion: completion, + model: model as string + }; + addUserFeedback(feedbackData); } }; - const addUserFeedback = async (sessionId: string, key: number, feedback: string) => { + const addUserFeedback = async (feedbackData: FeedbackData) => { if (!appContext) return; const apiClient = new ApiClient(appContext); - await apiClient.userFeedback.addUserFeedback({sessionId, key, feedback}); + await apiClient.userFeedback.addUserFeedback({feedbackData}); }; return ( @@ -497,8 +510,8 @@ export default function MultiChat() { key={idx} message={message} showMetadata={showMetadata} - onThumbsUp={() => handleFeedback('thumbsUp', idx, message)} - onThumbsDown={() => handleFeedback('thumbsDown', idx, message)} + onThumbsUp={() => handleFeedback('thumbsUp', idx, message, val)} + onThumbsDown={() => handleFeedback('thumbsDown', idx, message, val)} /> ))} diff --git a/lib/user-interface/react-app/src/components/chatbot/types.ts b/lib/user-interface/react-app/src/components/chatbot/types.ts index 747144eab..b31b3a37b 100644 --- a/lib/user-interface/react-app/src/components/chatbot/types.ts +++ b/lib/user-interface/react-app/src/components/chatbot/types.ts @@ -147,3 +147,12 @@ export enum ChabotOutputModality { Image = "IMAGE", Embedding = "EMBEDDING", } + +export interface FeedbackData { + sessionId: string; + key: number; + feedback: string; + prompt: string; + completion: string; + model: string; +} From f246de34482ef7a472fceafe9cb568ae8107cc5c Mon Sep 17 00:00:00 2001 From: Ajay Lamba Date: Mon, 12 Feb 2024 21:21:34 +0530 Subject: [PATCH 3/3] Changing the user feedback type. Changed the user feedback type from string to number as number are easy to handle in Athena and other such tools. --- lib/chatbot-api/chatbot-s3-buckets/index.ts | 15 --------------- .../src/components/chatbot/chat-message.tsx | 14 +++++++------- .../react-app/src/components/chatbot/chat.tsx | 6 +++--- .../src/components/chatbot/multi-chat.tsx | 6 +++--- .../react-app/src/components/chatbot/types.ts | 2 +- 5 files changed, 14 insertions(+), 29 deletions(-) diff --git a/lib/chatbot-api/chatbot-s3-buckets/index.ts b/lib/chatbot-api/chatbot-s3-buckets/index.ts index 662badf34..e7666d048 100644 --- a/lib/chatbot-api/chatbot-s3-buckets/index.ts +++ b/lib/chatbot-api/chatbot-s3-buckets/index.ts @@ -44,23 +44,8 @@ export class ChatBotS3Buckets extends Construct { blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, removalPolicy: cdk.RemovalPolicy.DESTROY, autoDeleteObjects: true, - transferAcceleration: true, enforceSSL: true, serverAccessLogsBucket: logsBucket, - cors: [ - { - allowedHeaders: ["*"], - allowedMethods: [ - s3.HttpMethods.PUT, - s3.HttpMethods.POST, - s3.HttpMethods.GET, - s3.HttpMethods.HEAD, - ], - allowedOrigins: ["*"], - exposedHeaders: ["ETag"], - maxAge: 3000, - }, - ], }); this.filesBucket = filesBucket; diff --git a/lib/user-interface/react-app/src/components/chatbot/chat-message.tsx b/lib/user-interface/react-app/src/components/chatbot/chat-message.tsx index 90f294476..2649bebc4 100644 --- a/lib/user-interface/react-app/src/components/chatbot/chat-message.tsx +++ b/lib/user-interface/react-app/src/components/chatbot/chat-message.tsx @@ -43,7 +43,7 @@ export default function ChatMessage(props: ChatMessageProps) { const [files, setFiles] = useState([] as ImageFile[]); const [documentIndex, setDocumentIndex] = useState("0"); const [promptIndex, setPromptIndex] = useState("0"); - const [selectedIcon, setSelectedIcon] = useState<'thumbsUp' | 'thumbsDown' | null>(null); + const [selectedIcon, setSelectedIcon] = useState<1 | 0 | null>(null); useEffect(() => { const getSignedUrls = async () => { @@ -275,21 +275,21 @@ export default function ChatMessage(props: ChatMessageProps) { }} />
- {(selectedIcon === 'thumbsUp' || selectedIcon === null) && ( + {(selectedIcon === 1 || selectedIcon === null) && ( { props.onThumbsUp(); - setSelectedIcon('thumbsUp'); + setSelectedIcon(1); }} /> )} - {(selectedIcon === 'thumbsDown' || selectedIcon === null) && ( + {(selectedIcon === 0 || selectedIcon === null) && ( { props.onThumbsDown(); - setSelectedIcon('thumbsDown'); + setSelectedIcon(0); }} /> )} diff --git a/lib/user-interface/react-app/src/components/chatbot/chat.tsx b/lib/user-interface/react-app/src/components/chatbot/chat.tsx index e888da028..9f7b6bf85 100644 --- a/lib/user-interface/react-app/src/components/chatbot/chat.tsx +++ b/lib/user-interface/react-app/src/components/chatbot/chat.tsx @@ -80,7 +80,7 @@ export default function Chat(props: { sessionId?: string }) { })(); }, [appContext, props.sessionId]); - const handleFeedback = (feedbackType: 'thumbsUp' | 'thumbsDown', idx: number, message: ChatBotHistoryItem) => { + const handleFeedback = (feedbackType: 1 | 0, idx: number, message: ChatBotHistoryItem) => { if (message.metadata.sessionId) { const prompt = messageHistory[idx - 1]?.content; const completion = message.content; @@ -112,8 +112,8 @@ export default function Chat(props: { sessionId?: string }) { key={idx} message={message} showMetadata={configuration.showMetadata} - onThumbsUp={() => handleFeedback('thumbsUp', idx, message)} - onThumbsDown={() => handleFeedback('thumbsDown', idx, message)} + onThumbsUp={() => handleFeedback(1, idx, message)} + onThumbsDown={() => handleFeedback(0, idx, message)} /> ))} diff --git a/lib/user-interface/react-app/src/components/chatbot/multi-chat.tsx b/lib/user-interface/react-app/src/components/chatbot/multi-chat.tsx index e6f604ecc..66b375c6d 100644 --- a/lib/user-interface/react-app/src/components/chatbot/multi-chat.tsx +++ b/lib/user-interface/react-app/src/components/chatbot/multi-chat.tsx @@ -330,7 +330,7 @@ export default function MultiChat() { [ReadyState.UNINSTANTIATED]: "Uninstantiated", }[readyState]; - const handleFeedback = (feedbackType: 'thumbsUp' | 'thumbsDown', idx: number, message: ChatBotHistoryItem, messageHistory: ChatBotHistoryItem[]) => { + const handleFeedback = (feedbackType: 1 | 0, idx: number, message: ChatBotHistoryItem, messageHistory: ChatBotHistoryItem[]) => { console.log("Message history: ", messageHistory); if (message.metadata.sessionId) { const prompt = messageHistory[idx - 1]?.content; @@ -510,8 +510,8 @@ export default function MultiChat() { key={idx} message={message} showMetadata={showMetadata} - onThumbsUp={() => handleFeedback('thumbsUp', idx, message, val)} - onThumbsDown={() => handleFeedback('thumbsDown', idx, message, val)} + onThumbsUp={() => handleFeedback(1, idx, message, val)} + onThumbsDown={() => handleFeedback(0, idx, message, val)} /> ))} diff --git a/lib/user-interface/react-app/src/components/chatbot/types.ts b/lib/user-interface/react-app/src/components/chatbot/types.ts index b31b3a37b..d61ffa2fe 100644 --- a/lib/user-interface/react-app/src/components/chatbot/types.ts +++ b/lib/user-interface/react-app/src/components/chatbot/types.ts @@ -151,7 +151,7 @@ export enum ChabotOutputModality { export interface FeedbackData { sessionId: string; key: number; - feedback: string; + feedback: number; prompt: string; completion: string; model: string;