Like/Dislike Button for Chatbot (#5391)

This commit is contained in:
Dawood Khan 2023-09-01 19:20:55 -04:00 committed by GitHub
parent ca2aa8b25b
commit abf1c57d7d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 236 additions and 35 deletions

View File

@ -0,0 +1,10 @@
---
"@gradio/chatbot": minor
"@gradio/icons": minor
"@gradio/utils": minor
"gradio": minor
---
highlight:
#### Like/Dislike Button for Chatbot

View File

@ -60,7 +60,7 @@ from gradio.components import (
component,
)
from gradio.deploy_space import deploy
from gradio.events import SelectData
from gradio.events import LikeData, SelectData
from gradio.exceptions import Error
from gradio.external import load
from gradio.flagging import (

View File

@ -16,6 +16,7 @@ from gradio.deprecation import warn_deprecation, warn_style_method_deprecation
from gradio.events import (
Changeable,
EventListenerMethod,
Likeable,
Selectable,
)
@ -23,7 +24,7 @@ set_documentation_group("component")
@document()
class Chatbot(Changeable, Selectable, IOComponent, JSONSerializable):
class Chatbot(Changeable, Selectable, Likeable, IOComponent, JSONSerializable):
"""
Displays a chatbot output showing both user submitted messages and responses. Supports a subset of Markdown including bold, italics, code, tables. Also supports audio/video/image files, which are displayed in the Chatbot, and other kinds of files which are displayed as links.
Preprocessing: passes the messages in the Chatbot as a {List[List[str | None | Tuple]]}, i.e. a list of lists. The inner list has 2 elements: the user message and the response message. See `Postprocessing` for the format of these messages.
@ -89,6 +90,12 @@ class Chatbot(Changeable, Selectable, IOComponent, JSONSerializable):
Uses event data gradio.SelectData to carry `value` referring to text of selected message, and `index` tuple to refer to [message, participant] index.
See EventData documentation on how to use this event data.
"""
self.like: EventListenerMethod
"""
Event listener for when the user likes or dislikes a message from Chatbot.
Uses event data gradio.LikeData to carry `value` referring to text of selected message, `index` tuple to refer to [message, participant] index, and `liked` bool which is True if the item was liked, False if disliked.
See EventData documentation on how to use this event data.
"""
self.height = height
self.rtl = rtl
if latex_delimiters is None:
@ -123,6 +130,7 @@ class Chatbot(Changeable, Selectable, IOComponent, JSONSerializable):
"value": self.value,
"latex_delimiters": self.latex_delimiters,
"selectable": self.selectable,
"likeable": self.likeable,
"height": self.height,
"show_share_button": self.show_share_button,
"rtl": self.rtl,

View File

@ -346,3 +346,34 @@ class SelectData(EventData):
"""
True if the item was selected, False if deselected.
"""
@document("*like", inherit=True)
class Likeable(EventListener):
def __init__(self):
self.likeable: bool = False
self.like = EventListenerMethod(
self, "like", callback=lambda: setattr(self, "likeable", True)
)
"""
This listener is triggered when the user likes/dislikes from within the Component.
This event has EventData of type gradio.LikeData that carries information, accessible through LikeData.index and LikeData.value.
See EventData documentation on how to use this event data.
"""
class LikeData(EventData):
def __init__(self, target: Block | None, data: Any):
super().__init__(target, data)
self.index: int | tuple[int, int] = data["index"]
"""
The index of the liked/disliked item. Is a tuple if the component is two dimensional.
"""
self.value: Any = data["value"]
"""
The value of the liked/disliked item.
"""
self.liked: bool = data.get("liked", True)
"""
True if the item was liked, False if disliked.
"""

View File

@ -50,6 +50,36 @@ Of course, in practice, you would replace `bot()` with your own more complex fun
Finally, we enable queuing by running `demo.queue()`, which is required for streaming intermediate outputs. You can try the improved chatbot by scrolling to the demo at the top of this page.
## Liking / Disliking Chat Messages
Once you've created your `gr.Chatbot`, you can add the ability for users to like or dislike messages. This can be useful if you would like users to vote on a bot's responses or flag inappropriate results.
To add this functionality to your Chatbot, simply attach a `.like()` event to your Chatbot. A chatbot that has the `.like()` event will automatically feature a thumbs-up icon and a thumbs-down icon next to every bot message.
The `.like()` method requires you to pass in a function that is called when a user clicks on these icons. In your function, you should have an argument whose type is `gr.LikeData`. Gradio will automatically supply the parameter to this argument with an object that contains information about the liked or disliked message. Here's a simplistic example of how you can have users like or dislike chat messages:
```py
import gradio as gr
def greet(history, input):
return history + [(input, "Hello, " + input)]
def vote(data: gr.LikeData):
if data.liked:
print("You upvoted this response: " + data.value)
else:
print("You downvoted this response: " + data.value)
with gr.Blocks() as demo:
chatbot = gr.Chatbot()
textbox = gr.Textbox()
textbox.submit(greet, [chatbot, textbox], [chatbot])
chatbot.like(vote, None, None) # Adding this line causes the like/dislike icons to appear in your chatbot
demo.launch()
```
## Adding Markdown, Images, Audio, or Videos
The `gr.Chatbot` component supports a subset of markdown including bold, italics, and code. For example, we could write a function that responds to a user's message, with a bold **That's cool!**, like this:

View File

@ -49,8 +49,8 @@ describe("Chatbot", () => {
const user = getAllByTestId("user");
const bot = getAllByTestId("bot");
assert.equal(user[0].innerHTML, "");
assert.equal(bot[0].innerHTML, "");
assert.isFalse(user[0].innerHTML.includes("span"));
assert.isFalse(bot[0].innerHTML.includes("span"));
});
test("renders additional message as they are passed", async () => {

View File

@ -4,11 +4,12 @@
import { beforeUpdate, afterUpdate, createEventDispatcher } from "svelte";
import { ShareButton } from "@gradio/atoms";
import type { SelectData } from "@gradio/utils";
import type { SelectData, LikeData } from "@gradio/utils";
import type { FileData } from "@gradio/upload";
import { MarkdownCode as Markdown } from "@gradio/markdown/static";
import { get_fetchable_url_or_file } from "@gradio/upload";
import Copy from "./Copy.svelte";
import { Like, Dislike, Check } from "@gradio/icons";
export let value:
| [string | FileData | null, string | FileData | null][]
@ -21,8 +22,8 @@
display: boolean;
}[];
export let pending_message = false;
export let feedback: string[] | null = null;
export let selectable = false;
export let likeable = false;
export let show_share_button = false;
export let rtl = false;
export let show_copy_button = false;
@ -38,6 +39,7 @@
const dispatch = createEventDispatcher<{
change: undefined;
select: SelectData;
like: LikeData;
}>();
beforeUpdate(() => {
@ -78,6 +80,19 @@
value: message
});
}
function handle_like(
i: number,
j: number,
message: string | FileData | null,
liked: boolean
): void {
dispatch("like", {
index: [i, j],
value: message,
liked: liked
});
}
</script>
{#if show_share_button && value !== null && value.length > 0}
@ -125,6 +140,28 @@
on:click={() => handle_select(i, j, message)}
dir={rtl ? "rtl" : "ltr"}
>
<div
class="message-buttons-{j == 0 ? 'user' : 'bot'}"
class:message-buttons-fit={!bubble_full_width}
class:hide={message === null}
>
{#if likeable && j == 1}
<div class="like">
<button on:click={() => handle_like(i, j, message, true)}
><Like /></button
>
<button on:click={() => handle_like(i, j, message, false)}
><Dislike /></button
>
</div>
{/if}
{#if show_copy_button && message && typeof message === "string"}
<div class="icon-button">
<Copy value={message} />
</div>
{/if}
</div>
{#if typeof message === "string"}
<Markdown
{message}
@ -132,19 +169,6 @@
{sanitize_html}
on:load={scroll}
/>
{#if feedback && j == 1}
<div class="feedback">
{#each feedback as f}
<button>{f}</button>
{/each}
</div>
{/if}
{#if show_copy_button && message}
<div class="icon-button">
<Copy value={message} />
</div>
{/if}
{:else if message !== null && message.mime_type?.includes("audio")}
<audio
data-testid="chatbot-audio"
@ -311,20 +335,37 @@
border-radius: 50%;
}
.feedback {
.message-buttons-user,
.message-buttons-bot {
display: flex;
position: absolute;
top: var(--spacing-xl);
right: calc(var(--spacing-xxl) + var(--spacing-xl));
gap: var(--spacing-lg);
font-size: var(--text-sm);
position: relative;
justify-content: flex-end;
}
.feedback button {
.message-buttons-bot {
margin-right: 15px;
}
.message-buttons-fit {
margin-right: 0px;
}
.icon-button {
margin-top: -10px;
margin-bottom: -10px;
}
.like {
display: flex;
height: var(--size-8);
width: var(--size-8);
margin-top: -10px;
margin-bottom: -10px;
}
.like button {
color: var(--body-text-color-subdued);
}
.feedback button:hover {
.like button:hover,
button:focus {
color: var(--body-text-color);
}
.selectable {
cursor: pointer;
}
@ -450,10 +491,4 @@
.message-wrap :global(pre) {
position: relative;
}
.icon-button {
position: absolute;
top: 6px;
right: 6px;
}
</style>

View File

@ -1,5 +1,5 @@
<script lang="ts">
import type { Gradio, SelectData } from "@gradio/utils";
import type { Gradio, SelectData, LikeData } from "@gradio/utils";
import ChatBot from "./ChatBot.svelte";
import { Block, BlockLabel } from "@gradio/atoms";
@ -20,6 +20,7 @@
export let root: string;
export let root_url: null | string;
export let selectable = false;
export let likeable = false;
export let show_share_button = false;
export let rtl = false;
export let show_copy_button = false;
@ -35,6 +36,7 @@
select: SelectData;
share: ShareData;
error: string;
like: LikeData;
}>;
export let avatar_images: [string | null, string | null] = [null, null];
@ -87,6 +89,7 @@
{/if}
<ChatBot
{selectable}
{likeable}
{show_share_button}
value={_value}
{latex_delimiters}
@ -95,6 +98,7 @@
{show_copy_button}
on:change={() => gradio.dispatch("change", value)}
on:select={(e) => gradio.dispatch("select", e.detail)}
on:like={(e) => gradio.dispatch("like", e.detail)}
on:share={(e) => gradio.dispatch("share", e.detail)}
on:error={(e) => gradio.dispatch("error", e.detail)}
{avatar_images}

View File

@ -0,0 +1,38 @@
<svg
viewBox="0 -3 32 32"
height="100%"
width="100%"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
fill="currentColor"
><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g
id="SVGRepo_tracerCarrier"
stroke-linecap="round"
stroke-linejoin="round"
></g><g id="SVGRepo_iconCarrier">
<title>dislike [#1388]</title> <desc>Created with Sketch.</desc>
<defs> </defs>
<g
id="Page-1"
stroke="none"
stroke-width="1"
fill="none"
fill-rule="evenodd"
>
<g
id="Dribbble-Light-Preview"
transform="translate(-139.000000, -760.000000)"
fill="currentColor"
>
<g id="icons" transform="translate(56.000000, 160.000000)">
<path
d="M101.900089,600 L99.8000892,600 L99.8000892,611.987622 L101.900089,611.987622 C103.060339,611.987622 104.000088,611.093545 104.000088,609.989685 L104.000088,601.997937 C104.000088,600.894077 103.060339,600 101.900089,600 M87.6977917,600 L97.7000896,600 L97.7000896,611.987622 L95.89514,618.176232 C95.6819901,619.491874 94.2455904,620.374962 92.7902907,619.842512 C91.9198408,619.52484 91.400091,618.66273 91.400091,617.774647 L91.400091,612.986591 C91.400091,612.43516 90.9296911,611.987622 90.3500912,611.987622 L85.8728921,611.987622 C84.0259425,611.987622 82.6598928,610.35331 83.0746427,608.641078 L84.8995423,602.117813 C85.1998423,600.878093 86.360092,600 87.6977917,600"
id="dislike-[#1388]"
>
</path>
</g>
</g>
</g>
</g></svg
>

After

Width:  |  Height:  |  Size: 1.5 KiB

37
js/icons/src/Like.svelte Normal file
View File

@ -0,0 +1,37 @@
<svg
viewBox="0 0 32 32"
height="100%"
width="100%"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
fill="currentColor"
><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g
id="SVGRepo_tracerCarrier"
stroke-linecap="round"
stroke-linejoin="round"
></g><g id="SVGRepo_iconCarrier">
<title>like [#1385]</title> <desc>Created with Sketch.</desc> <defs> </defs>
<g
id="Page-1"
stroke="none"
stroke-width="1"
fill="none"
fill-rule="evenodd"
>
<g
id="Dribbble-Light-Preview"
transform="translate(-259.000000, -760.000000)"
fill="currentColor"
>
<g id="icons" transform="translate(56.000000, 160.000000)">
<path
d="M203,620 L207.200006,620 L207.200006,608 L203,608 L203,620 Z M223.924431,611.355 L222.100579,617.89 C221.799228,619.131 220.638976,620 219.302324,620 L209.300009,620 L209.300009,608.021 L211.104962,601.825 C211.274012,600.775 212.223214,600 213.339366,600 C214.587817,600 215.600019,600.964 215.600019,602.153 L215.600019,608 L221.126177,608 C222.97313,608 224.340232,609.641 223.924431,611.355 L223.924431,611.355 Z"
id="like-[#1385]"
>
</path>
</g>
</g>
</g>
</g></svg
>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -10,6 +10,7 @@ export { default as Code } from "./Code.svelte";
export { default as Color } from "./Color.svelte";
export { default as Community } from "./Community.svelte";
export { default as Copy } from "./Copy.svelte";
export { default as Dislike } from "./Dislike.svelte";
export { default as Download } from "./Download.svelte";
export { default as DropdownArrow } from "./DropdownArrow.svelte";
export { default as Edit } from "./Edit.svelte";
@ -18,6 +19,7 @@ export { default as File } from "./File.svelte";
export { default as Info } from "./Info.svelte";
export { default as Image } from "./Image.svelte";
export { default as JSON } from "./JSON.svelte";
export { default as Like } from "./Like.svelte";
export { default as LineChart } from "./LineChart.svelte";
export { default as Maximise } from "./Maximise.svelte";
export { default as Music } from "./Music.svelte";

View File

@ -62,4 +62,3 @@ in two separate lines.`
latex_delimiters: [{ left: "$", right: "$", display: false }]
}}
/>

View File

@ -5,6 +5,12 @@ export interface SelectData {
selected?: boolean;
}
export interface LikeData {
index: number | [number, number];
value: any;
liked?: boolean;
}
export interface ShareData {
description: string;
title?: string;

View File

@ -2015,6 +2015,7 @@ class TestChatbot:
"root_url": None,
"selectable": False,
"latex_delimiters": [{"display": True, "left": "$$", "right": "$$"}],
"likeable": False,
"rtl": False,
"show_copy_button": False,
"avatar_images": (None, None),