Skip to content

Commit

Permalink
feat: Feedback (#866)
Browse files Browse the repository at this point in the history
  • Loading branch information
Yonom authored Sep 21, 2024
1 parent 4e60d3a commit 926dce5
Show file tree
Hide file tree
Showing 25 changed files with 449 additions and 15 deletions.
6 changes: 6 additions & 0 deletions .changeset/beige-rockets-lie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@assistant-ui/react-playground": patch
"@assistant-ui/react": patch
---

feat: Feedback Primtives, UI and Adapter
5 changes: 5 additions & 0 deletions apps/docs/app/(home)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,11 @@ const MyRuntimeProvider = ({ children }: { children: React.ReactNode }) => {
new SimpleImageAttachmentAdapter(),
new SimpleTextAttachmentAdapter(),
]),
feedback: {
submit: ({ message, type }) => {
console.log({ message, type });
},
},
},
});
return (
Expand Down
102 changes: 101 additions & 1 deletion apps/docs/content/docs/ui/primitives/ActionBar.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,15 @@ This primitive renders a `<button>` element unless `asChild` is set.
]}
/>

<DataAttributesTable
data={[
{
attribute: "[data-copied]",
values: "Present when the message was recently copied.",
},
]}
/>

#### Copied state

Show a different icon for a few seconds after the message is copied.
Expand All @@ -201,6 +210,15 @@ Show a different icon for a few seconds after the message is copied.
</ActionBarPrimitive.Copy>
```

or using the `data-copied` attribute:

```tsx
<ActionBarPrimitive.Copy className="group">
<CopyIcon className="group-data-[copied]:hidden" />
<CheckIcon className="hidden group-data-[copied]:block" />
</ActionBarPrimitive.Copy>
```

#### `useActionBarCopy`

Provides the `Copy` functionality as a hook.
Expand Down Expand Up @@ -293,4 +311,86 @@ const StopSpeaking = () => {

return <button onClick={stopSpeaking}>Stop speaking</button>;
};
```
```

### Feedback Positive

Shows a positive feedback submission button.

This primitive renders a `<button>` element unless `asChild` is set.

<ParametersTable
type="ActionBarPrimitiveFeedbackPositiveProps"
parameters={[
{
name: "asChild",
},
]}
/>

<DataAttributesTable
data={[
{
attribute: "[data-submitted]",
values: "Present when positive feedback was submitted.",
},
]}
/>

#### `useActionBarFeedbackPositive`

Provides the `FeedbackPositive` functionality as a hook.

```tsx
import { useActionBarFeedbackPositive } from "@assistant-ui/react";

const FeedbackPositive = () => {
const feedbackPositive = useActionBarFeedbackPositive();

// feedbackPositive action is not available
if (!feedbackPositive) return null;

return <button onClick={feedbackPositive}>Feedback Positive</button>;
};
```

### Feedback Negative

Shows a negative feedback submission button.

This primitive renders a `<button>` element unless `asChild` is set.

<ParametersTable
type="ActionBarPrimitiveFeedbackNegativeProps"
parameters={[
{
name: "asChild",
},
]}
/>

<DataAttributesTable
data={[
{
attribute: "[data-submitted]",
values: "Present when negative feedback was submitted.",
},
]}
/>

#### `useActionBarFeedbackNegative`

Provides the `FeedbackNegative` functionality as a hook.

```tsx
import { useActionBarFeedbackNegative } from "@assistant-ui/react";

const FeedbackNegative = () => {
const feedbackNegative = useActionBarFeedbackNegative();

// feedbackNegative action is not available
if (!feedbackNegative) return null;

return <button onClick={feedbackNegative}>Feedback Negative</button>;
};
```
12 changes: 11 additions & 1 deletion apps/docs/content/docs/ui/styled/Decomposition.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -288,8 +288,10 @@ const MyAssistantActionBar: FC = () => {
autohideFloat="single-branch"
>
<AssistantActionBar.SpeechControl />
<AssistantActionBar.Reload />
<AssistantActionBar.Copy />
<AssistantActionBar.Reload />
<AssistantActionBar.FeedbackPositive />
<AssistantActionBar.FeedbackNegative />
</AssistantActionBar.Root>
);
};
Expand Down Expand Up @@ -331,6 +333,14 @@ Shows a speak button.

Shows a stop speaking button.

### AssistantActionBar.FeedbackPositive

Shows a positive feedback button.

### AssistantActionBar.FeedbackNegative

Shows a negative feedback button.

## BranchPicker

Renders the branch picker.
Expand Down
5 changes: 5 additions & 0 deletions packages/react-playground/src/lib/playground-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ const CAPABILITIES = Object.freeze({
unstable_copy: true,
speak: false,
attachments: false,
feedback: false,
});

const EMPTY_BRANCHES: readonly string[] = Object.freeze([]);
Expand Down Expand Up @@ -266,6 +267,10 @@ export class PlaygroundThreadRuntime implements ReactThreadRuntime {
throw new Error("PlaygroundRuntime does not support speaking.");
}

public submitFeedback(): never {
throw new Error("PlaygroundRuntime does not support feedback.");
}

public deleteMessage(messageId: string) {
this.setMessages(this.messages.filter((m) => m.id !== messageId));
}
Expand Down
7 changes: 7 additions & 0 deletions packages/react/src/context/stores/MessageUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ export type MessageUtilsState = Readonly<{
isSpeaking: boolean;
stopSpeaking: () => void;
addUtterance: (utterance: SpeechSynthesisAdapter.Utterance) => void;

submittedFeedback: "positive" | "negative" | null;
setSubmittedFeedback: (feedback: "positive" | "negative" | null) => void;
}>;

export const makeMessageUtilsStore = () =>
Expand All @@ -35,5 +38,9 @@ export const makeMessageUtilsStore = () =>
set({ isSpeaking: false });
});
},
submittedFeedback: null,
setSubmittedFeedback: (feedback) => {
set({ submittedFeedback: feedback });
},
};
});
1 change: 1 addition & 0 deletions packages/react/src/context/stores/Thread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type RuntimeCapabilities = {
unstable_copy: boolean;
speak: boolean;
attachments: boolean;
feedback: boolean;
};

export const getThreadStateFromRuntime = (
Expand Down
10 changes: 10 additions & 0 deletions packages/react/src/context/stores/ThreadActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ export type AddToolResultOptions = {
result: any;
};

export type SubmitFeedbackOptions = {
messageId: string;
type: "negative" | "positive";
};

export type ThreadActionsState = Readonly<{
getBranches: (messageId: string) => readonly string[];
switchToBranch: (branchId: string) => void;
Expand All @@ -22,6 +27,8 @@ export type ThreadActionsState = Readonly<{
addToolResult: (options: AddToolResultOptions) => void;

speak: (messageId: string) => SpeechSynthesisAdapter.Utterance;

submitFeedback: (feedback: SubmitFeedbackOptions) => void;
}>;

export const makeThreadActionStore = (
Expand All @@ -42,6 +49,9 @@ export const makeThreadActionStore = (
runtimeStore.getState().addToolResult(options),

speak: (messageId) => runtimeStore.getState().speak(messageId),

submitFeedback: ({ messageId, type }) =>
runtimeStore.getState().submitFeedback({ messageId, type }),
}),
);
};
2 changes: 2 additions & 0 deletions packages/react/src/primitive-hooks/actionBar/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ export { useActionBarEdit } from "./useActionBarEdit";
export { useActionBarReload } from "./useActionBarReload";
export { useActionBarSpeak } from "./useActionBarSpeak";
export { useActionBarStopSpeaking } from "./useActionBarStopSpeaking";
export { useActionBarFeedbackPositive } from "./useActionBarFeedbackPositive";
export { useActionBarFeedbackNegative } from "./useActionBarFeedbackNegative";
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useCallback } from "react";
import { useThreadActions } from "../../context/react/ThreadContext";
import { useMessageStore, useMessageUtilsStore } from "../../context";

export const useActionBarFeedbackNegative = () => {
const threadActions = useThreadActions();
const messageStore = useMessageStore();
const messageUtilsStore = useMessageUtilsStore();

const callback = useCallback(() => {
threadActions.submitFeedback({
messageId: messageStore.getState().message.id,
type: "negative",
});
messageUtilsStore.getState().setSubmittedFeedback("negative");
}, [messageStore, messageUtilsStore, threadActions]);

return callback;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useCallback } from "react";
import { useThreadActions } from "../../context/react/ThreadContext";
import { useMessageStore, useMessageUtilsStore } from "../../context";

export const useActionBarFeedbackPositive = () => {
const threadActions = useThreadActions();
const messageStore = useMessageStore();
const messageUtilsStore = useMessageUtilsStore();

const callback = useCallback(() => {
threadActions.submitFeedback({
messageId: messageStore.getState().message.id,
type: "positive",
});
messageUtilsStore.getState().setSubmittedFeedback("positive");
}, [messageStore, messageUtilsStore, threadActions]);

return callback;
};
12 changes: 11 additions & 1 deletion packages/react/src/primitive-hooks/message/useMessageIf.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type MessageIfFilters = {
lastOrHover: boolean | undefined;
speaking: boolean | undefined;
hasAttachments: boolean | undefined;
submittedFeedback: "positive" | "negative" | null | undefined;
};
export type UseMessageIfProps = RequireAtLeastOne<MessageIfFilters>;

Expand All @@ -24,7 +25,10 @@ export const useMessageIf = (props: UseMessageIfProps) => {

return useCombinedStore(
[messageStore, messageUtilsStore],
({ message, branches, isLast }, { isCopied, isHovering, isSpeaking }) => {
(
{ message, branches, isLast },
{ isCopied, isHovering, isSpeaking, submittedFeedback },
) => {
if (props.hasBranches === true && branches.length < 2) return false;

if (props.user && message.role !== "user") return false;
Expand All @@ -51,6 +55,12 @@ export const useMessageIf = (props: UseMessageIfProps) => {
)
return false;

if (
props.submittedFeedback !== undefined &&
submittedFeedback !== props.submittedFeedback
)
return false;

return true;
},
);
Expand Down
35 changes: 26 additions & 9 deletions packages/react/src/primitives/actionBar/ActionBarCopy.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,34 @@
"use client";

import { forwardRef } from "react";
import { useActionBarCopy } from "../../primitive-hooks/actionBar/useActionBarCopy";
import {
ActionButtonProps,
createActionButton,
} from "../../utils/createActionButton";
import { ActionButtonProps } from "../../utils/createActionButton";
import { composeEventHandlers } from "@radix-ui/primitive";
import { Primitive } from "@radix-ui/react-primitive";
import { useMessageUtils } from "../../context";

export type ActionBarPrimitiveCopyProps = ActionButtonProps<
typeof useActionBarCopy
>;

export const ActionBarPrimitiveCopy = createActionButton(
"ActionBarPrimitive.Copy",
useActionBarCopy,
["copiedDuration"],
);
export const ActionBarPrimitiveCopy = forwardRef<
HTMLButtonElement,
Partial<ActionBarPrimitiveCopyProps>
>(({ copiedDuration, onClick, disabled, ...props }, forwardedRef) => {
const isCopied = useMessageUtils((u) => u.isCopied);
const callback = useActionBarCopy({ copiedDuration });
return (
<Primitive.button
type="button"
{...(isCopied ? { "data-copied": "true" } : {})}
{...props}
ref={forwardedRef}
disabled={disabled || !callback}
onClick={composeEventHandlers(onClick, () => {
callback?.();
})}
/>
);
});

ActionBarPrimitiveCopy.displayName = "ActionBarPrimitive.Copy";
Loading

0 comments on commit 926dce5

Please sign in to comment.