+
+
+
+
- {peekLocation && (
-
-
-
-
-
- ),
- }}>
-
-
-
- )}
-
-
+ ModalProps={{
+ keepMounted: true,
+ }}>
+
+ {peekLocation && (
+
+
+
+
+
+ ),
+ }}>
+
+
+
+ )}
+
+
+
);
};
diff --git a/weave-js/src/components/PagePanelComponents/Home/Browse3/context.tsx b/weave-js/src/components/PagePanelComponents/Home/Browse3/context.tsx
index 640e68441b6..4ea78bf6e78 100644
--- a/weave-js/src/components/PagePanelComponents/Home/Browse3/context.tsx
+++ b/weave-js/src/components/PagePanelComponents/Home/Browse3/context.tsx
@@ -324,7 +324,8 @@ export const browse3ContextGen = (
traceId: string,
callId: string,
path?: string | null,
- tracetree?: boolean
+ tracetree?: boolean,
+ feedbackExpand?: boolean
) => {
let url = `${projectRoot(entityName, projectName)}/calls/${callId}`;
const params = new URLSearchParams();
@@ -334,6 +335,9 @@ export const browse3ContextGen = (
if (tracetree !== undefined) {
params.set(TRACETREE_PARAM, tracetree ? '1' : '0');
}
+ if (feedbackExpand !== undefined) {
+ params.set(FEEDBACK_EXPAND_PARAM, feedbackExpand ? '1' : '0');
+ }
if (params.toString()) {
url += '?' + params.toString();
}
@@ -470,7 +474,8 @@ type RouteType = {
traceId: string,
callId: string,
path?: string | null,
- tracetree?: boolean
+ tracetree?: boolean,
+ feedbackExpand?: boolean
) => string;
tracesUIUrl: (entityName: string, projectName: string) => string;
callsUIUrl: (
@@ -528,6 +533,7 @@ const useSetSearchParam = () => {
export const PEEK_PARAM = 'peekPath';
export const TRACETREE_PARAM = 'tracetree';
+export const FEEDBACK_EXPAND_PARAM = 'feedbackExpand';
export const PATH_PARAM = 'path';
export const baseContext = browse3ContextGen(
diff --git a/weave-js/src/components/PagePanelComponents/Home/Browse3/feedback/FeedbackGridInner.tsx b/weave-js/src/components/PagePanelComponents/Home/Browse3/feedback/FeedbackGridInner.tsx
index fb5b0574564..846745585f7 100644
--- a/weave-js/src/components/PagePanelComponents/Home/Browse3/feedback/FeedbackGridInner.tsx
+++ b/weave-js/src/components/PagePanelComponents/Home/Browse3/feedback/FeedbackGridInner.tsx
@@ -26,7 +26,10 @@ export const FeedbackGridInner = ({
display: 'flex',
renderCell: params => (
-
+
),
},
@@ -42,6 +45,9 @@ export const FeedbackGridInner = ({
if (params.row.feedback_type === 'wandb.reaction.1') {
return params.row.payload.emoji;
}
+ if (params.row.feedback_type === 'wandb.structuredFeedback.1') {
+ return params.row.payload.name + ': ' + params.row.payload.value;
+ }
return
;
},
},
diff --git a/weave-js/src/components/PagePanelComponents/Home/Browse3/feedback/FeedbackTypeChip.tsx b/weave-js/src/components/PagePanelComponents/Home/Browse3/feedback/FeedbackTypeChip.tsx
index 2daeb94fda1..3507ee16bea 100644
--- a/weave-js/src/components/PagePanelComponents/Home/Browse3/feedback/FeedbackTypeChip.tsx
+++ b/weave-js/src/components/PagePanelComponents/Home/Browse3/feedback/FeedbackTypeChip.tsx
@@ -1,20 +1,56 @@
import React from 'react';
import {Pill, TagColorName} from '../../../../Tag';
+import { useWeaveflowRouteContext } from '../context';
+import { parseRef, refUri } from '@wandb/weave/react';
+import { Link } from '../../Browse2/CommonLib';
+import { TargetBlank } from '@wandb/weave/common/util/links';
+import { useHistory } from 'react-router-dom';
+
type FeedbackTypeChipProps = {
feedbackType: string;
+ feedbackRef?: string;
};
-export const FeedbackTypeChip = ({feedbackType}: FeedbackTypeChipProps) => {
+export const FeedbackTypeChip = ({
+ feedbackType,
+ feedbackRef,
+}: FeedbackTypeChipProps) => {
+ const {baseRouter} = useWeaveflowRouteContext();
+ const history = useHistory();
+
let color: TagColorName = 'teal';
- let label = feedbackType;
+ let label = feedbackType;
if (feedbackType === 'wandb.reaction.1') {
color = 'purple';
label = 'Reaction';
} else if (feedbackType === 'wandb.note.1') {
color = 'gold';
label = 'Note';
+ } else if (feedbackType === 'wandb.structuredFeedback.1') {
+ color = 'moon';
+ label = 'Structured';
+ const objRef = feedbackRef ? parseRef(feedbackRef) : undefined;
+ if (!objRef) {
+ return
;
+ }
+
+ const onClick = () => history.replace(
+ baseRouter.objectVersionUIUrl(
+ objRef.entityName,
+ objRef.projectName,
+ (objRef.artifactName ?? '') + '-obj',
+ objRef.artifactVersion,
+ )
+ );
+ return (
+
+
+
+ );
}
- return
;
+ return (
+
+ );
};
diff --git a/weave-js/src/components/PagePanelComponents/Home/Browse3/feedback/StructuredFeedback/AddColumnButton.tsx b/weave-js/src/components/PagePanelComponents/Home/Browse3/feedback/StructuredFeedback/AddColumnButton.tsx
new file mode 100644
index 00000000000..6fef4c678db
--- /dev/null
+++ b/weave-js/src/components/PagePanelComponents/Home/Browse3/feedback/StructuredFeedback/AddColumnButton.tsx
@@ -0,0 +1,577 @@
+import {
+ Dialog,
+ DialogActions as MaterialDialogActions,
+ DialogContent as MaterialDialogContent,
+ DialogTitle as MaterialDialogTitle,
+} from '@material-ui/core';
+import {Autocomplete, TextField as MuiTextField} from '@mui/material';
+import {MOON_300} from '@wandb/weave/common/css/color.styles';
+import {Button} from '@wandb/weave/components/Button';
+import {TextField} from '@wandb/weave/components/Form/TextField';
+import {Tailwind} from '@wandb/weave/components/Tailwind';
+import React, {useState} from 'react';
+import styled from 'styled-components';
+
+import {useGetTraceServerClientContext} from '../../pages/wfReactInterface/traceServerClientContext';
+
+type BaseFeedback = {
+ name: string;
+ ref?: string;
+};
+
+type NumericalFeedback = BaseFeedback & {
+ type: 'NumericalFeedback';
+ min: number;
+ max: number;
+};
+
+type TextFeedback = BaseFeedback & {
+ type: 'TextFeedback';
+};
+
+type CategoricalFeedback = BaseFeedback & {
+ type: 'CategoricalFeedback';
+ options: string[];
+ multiSelect: boolean;
+ addNewOption: boolean;
+};
+
+type BooleanFeedback = BaseFeedback & {
+ type: 'BooleanFeedback';
+};
+
+type EmojiFeedback = BaseFeedback & {
+ type: 'EmojiFeedback';
+};
+
+type StructuredFeedback =
+ | CategoricalFeedback
+ | NumericalFeedback
+ | TextFeedback
+ | BooleanFeedback
+ | EmojiFeedback;
+
+type StructuredFeedbackSpec = {
+ _bases?: string[];
+ _class_name?: string;
+
+ types: StructuredFeedback[];
+ ref?: string;
+};
+
+const FEEDBACK_TYPE_OPTIONS = [
+ {name: 'Numerical feedback', value: 'NumericalFeedback'},
+ {name: 'Text feedback', value: 'TextFeedback'},
+ {name: 'Categorical feedback', value: 'CategoricalFeedback'},
+ {name: 'Boolean feedback', value: 'BooleanFeedback'},
+ {name: 'Emoji feedback', value: 'EmojiFeedback'},
+];
+
+const NumericalFeedbackComponent = ({
+ min,
+ max,
+ onSetMin,
+ onSetMax,
+}: {
+ min?: number;
+ max?: number;
+ onSetMin: (value: number) => void;
+ onSetMax: (value: number) => void;
+}) => (
+
+
optional
+
+
+ min
+ onSetMin(Number(value))}
+ placeholder="min"
+ />
+
+
+ max
+ onSetMax(Number(value))}
+ placeholder="max"
+ />
+
+
+
+);
+
+const CategoricalFeedbackComponent = ({
+ options,
+ setOptions,
+}: {
+ options: string[];
+ setOptions: (options: string[]) => void;
+}) => {
+ const [newOption, setNewOption] = useState
('');
+
+ return (
+
+
Add Options
+
+ setNewOption(value)}
+ placeholder="Enter option"
+ />
+
+
+
+ {options.map((option, index) => (
+
+ {option}
+
+ ))}
+
+
+ );
+};
+
+const createStructuredFeedback = (
+ type: string,
+ name: string,
+ min?: number,
+ max?: number,
+ options?: string[]
+): StructuredFeedback => {
+ // validate min and max
+ switch (type) {
+ case 'NumericalFeedback':
+ // validate min and max dont conflict
+ if (min && max && min > max) {
+ throw new Error('Min is greater than max');
+ }
+ return {type: 'NumericalFeedback', name, min: min!, max: max!};
+ case 'TextFeedback':
+ return {type: 'TextFeedback', name};
+ case 'CategoricalFeedback':
+ return {
+ type: 'CategoricalFeedback',
+ name,
+ options: options!,
+ multiSelect: false,
+ addNewOption: false,
+ };
+ case 'BooleanFeedback':
+ return {type: 'BooleanFeedback', name};
+ case 'EmojiFeedback':
+ return {type: 'EmojiFeedback', name};
+ default:
+ throw new Error('Invalid feedback type');
+ }
+};
+
+const FeedbackTypeSelector = ({
+ selectedFeedbackType,
+ setSelectedFeedbackType,
+ feedbackTypeOptions,
+ readOnly,
+}: {
+ selectedFeedbackType: string;
+ setSelectedFeedbackType: (value: string) => void;
+ feedbackTypeOptions: Array<{name: string; value: string}>;
+ readOnly?: boolean;
+}) => {
+ return (
+
+
Metric type
+
option.name}
+ onChange={(e, newValue) =>
+ setSelectedFeedbackType(newValue?.value ?? '')
+ }
+ value={feedbackTypeOptions.find(
+ option => option.value === selectedFeedbackType
+ )}
+ renderInput={params => (
+
+ )}
+ disableClearable
+ sx={{
+ minWidth: '244px',
+ width: 'auto',
+ }}
+ fullWidth
+ ListboxProps={{
+ style: {
+ maxHeight: '200px',
+ },
+ }}
+ disabled={readOnly}
+ renderOption={(props, option) => (
+
+ {option.name || }
+
+ )}
+ />
+
+ );
+};
+
+const submitStructuredFeedback = (
+ entity: string,
+ project: string,
+ newFeedback: StructuredFeedback,
+ existingFeedbackColumns: StructuredFeedback[],
+ editColumnName: string | null,
+ getTsClient: () => any,
+ onClose: () => void
+) => {
+ const tsClient = getTsClient();
+ let updatedTypes: StructuredFeedback[];
+
+ if (editColumnName) {
+ updatedTypes = existingFeedbackColumns.map(t =>
+ t.name === editColumnName ? newFeedback : t
+ );
+ } else {
+ updatedTypes = [...existingFeedbackColumns, newFeedback];
+ }
+
+ const value: StructuredFeedbackSpec = {
+ _bases: ['StructuredFeedback', 'Object', 'BaseModel'],
+ _class_name: 'StructuredFeedback',
+ types: updatedTypes,
+ };
+
+ const req = {
+ obj: {
+ project_id: `${entity}/${project}`,
+ object_id: 'StructuredFeedback-obj',
+ val: value,
+ },
+ };
+
+ tsClient
+ .objCreate(req)
+ .then(() => {
+ onClose();
+ })
+ .catch((e: any) => {
+ console.error(
+ `Error ${editColumnName ? 'updating' : 'creating'} structured feedback`,
+ e
+ );
+ });
+};
+
+const CreateStructuredFeedbackModal = ({
+ entity,
+ project,
+ existingFeedbackColumns,
+ onClose,
+}: {
+ entity: string;
+ project: string;
+ existingFeedbackColumns: StructuredFeedback[];
+ onClose: () => void;
+}) => {
+ const [open, setOpen] = useState(true);
+ const [nameField, setNameField] = useState('');
+ const [selectedFeedbackType, setSelectedFeedbackType] =
+ useState('Numerical feedback');
+ const [minValue, setMinValue] = useState(undefined);
+ const [maxValue, setMaxValue] = useState(undefined);
+ const [categoricalOptions, setCategoricalOptions] = useState([]);
+ const getTsClient = useGetTraceServerClientContext();
+
+ const submit = () => {
+ const option = FEEDBACK_TYPE_OPTIONS.find(
+ o => o.value === selectedFeedbackType
+ );
+ if (!option) {
+ console.error(
+ `Invalid feedback type: ${selectedFeedbackType}, options: ${FEEDBACK_TYPE_OPTIONS.map(
+ o => o.value
+ ).join(', ')}`
+ );
+ return;
+ }
+ let newFeedback;
+ try {
+ newFeedback = createStructuredFeedback(
+ option.value,
+ nameField,
+ minValue,
+ maxValue,
+ categoricalOptions
+ );
+ } catch (e) {
+ console.error(e);
+ return;
+ }
+ submitStructuredFeedback(
+ entity,
+ project,
+ newFeedback,
+ existingFeedbackColumns,
+ null,
+ getTsClient,
+ onClose
+ );
+ };
+
+ return (
+
+ );
+};
+
+const EditStructuredFeedbackModal = ({
+ entity,
+ project,
+ feedbackColumn,
+ structuredFeedbackData,
+ onClose,
+}: {
+ entity: string;
+ project: string;
+ feedbackColumn: StructuredFeedback;
+ structuredFeedbackData: StructuredFeedbackSpec;
+ onClose: () => void;
+}) => {
+ const [open, setOpen] = useState(true);
+ const [nameField, setNameField] = useState(feedbackColumn.name);
+ const [selectedFeedbackType, setSelectedFeedbackType] = useState(
+ feedbackColumn.type
+ );
+ const [minValue, setMinValue] = useState(
+ 'min' in feedbackColumn ? feedbackColumn.min : undefined
+ );
+ const [maxValue, setMaxValue] = useState(
+ 'max' in feedbackColumn ? feedbackColumn.max : undefined
+ );
+ const [categoricalOptions, setCategoricalOptions] = useState(
+ 'options' in feedbackColumn ? feedbackColumn.options : []
+ );
+
+ const getTsClient = useGetTraceServerClientContext();
+
+ const submit = () => {
+ let updatedFeedbackColumn;
+ try {
+ updatedFeedbackColumn = createStructuredFeedback(
+ selectedFeedbackType,
+ nameField,
+ minValue,
+ maxValue,
+ categoricalOptions
+ );
+ } catch (e) {
+ console.error(e);
+ return;
+ }
+ submitStructuredFeedback(
+ entity,
+ project,
+ updatedFeedbackColumn,
+ structuredFeedbackData.types,
+ feedbackColumn.name,
+ getTsClient,
+ onClose
+ );
+ };
+
+ return (
+
+ );
+};
+
+export const ConfigureStructuredFeedbackModal = ({
+ entity,
+ project,
+ structuredFeedbackData,
+ editColumnName,
+ onClose,
+}: {
+ entity: string;
+ project: string;
+ structuredFeedbackData?: StructuredFeedbackSpec;
+ editColumnName?: string;
+ onClose: () => void;
+}) => {
+ if (editColumnName && structuredFeedbackData) {
+ const feedbackColumn = structuredFeedbackData?.types.find(
+ t => t.name === editColumnName
+ );
+ if (!feedbackColumn) {
+ console.error(`Feedback column not found: ${editColumnName}`);
+ return null;
+ }
+ return (
+
+ );
+ } else {
+ return (
+
+ );
+ }
+};
+
+const DialogContent = styled(MaterialDialogContent)`
+ padding: 0 32px !important;
+`;
+DialogContent.displayName = 'S.DialogContent';
+
+const DialogTitle = styled(MaterialDialogTitle)`
+ padding: 32px 32px 16px 32px !important;
+
+ h2 {
+ font-weight: 600;
+ font-size: 24px;
+ line-height: 30px;
+ }
+`;
+DialogTitle.displayName = 'S.DialogTitle';
+
+const DialogActions = styled(MaterialDialogActions)<{$align: string}>`
+ justify-content: ${({$align}) =>
+ $align === 'left' ? 'flex-start' : 'flex-end'} !important;
+ padding: 32px 32px 32px 32px !important;
+`;
+DialogActions.displayName = 'S.DialogActions';
diff --git a/weave-js/src/components/PagePanelComponents/Home/Browse3/feedback/StructuredFeedback/StructuredFeedback.tsx b/weave-js/src/components/PagePanelComponents/Home/Browse3/feedback/StructuredFeedback/StructuredFeedback.tsx
new file mode 100644
index 00000000000..cdb9403772f
--- /dev/null
+++ b/weave-js/src/components/PagePanelComponents/Home/Browse3/feedback/StructuredFeedback/StructuredFeedback.tsx
@@ -0,0 +1,506 @@
+import {Checkbox} from '@mui/material';
+import {Autocomplete, TextField as MuiTextField} from '@mui/material';
+import {MOON_300} from '@wandb/weave/common/css/color.styles';
+import {TextField} from '@wandb/weave/components/Form/TextField';
+import {LoadingDots} from '@wandb/weave/components/LoadingDots';
+import {Tailwind} from '@wandb/weave/components/Tailwind';
+import debounce from 'lodash/debounce';
+import React, {
+ SyntheticEvent,
+ useCallback,
+ useEffect,
+ useMemo,
+ useState,
+} from 'react';
+
+import {useWFHooks} from '../../pages/wfReactInterface/context';
+import {useGetTraceServerClientContext} from '../../pages/wfReactInterface/traceServerClientContext';
+import {
+ FeedbackCreateError,
+ FeedbackCreateReq,
+ FeedbackCreateRes,
+ FeedbackCreateSuccess,
+ FeedbackReplaceReq,
+ FeedbackReplaceRes,
+} from '../../pages/wfReactInterface/traceServerClientTypes';
+
+// Constants
+const STRUCTURED_FEEDBACK_TYPE = 'wandb.structuredFeedback.1';
+const FEEDBACK_TYPES = {
+ NUMERICAL: 'NumericalFeedback',
+ TEXT: 'TextFeedback',
+ CATEGORICAL: 'CategoricalFeedback',
+ BOOLEAN: 'BooleanFeedback',
+};
+const DEBOUNCE_VAL = 150;
+
+// Interfaces
+interface StructuredFeedbackProps {
+ sfData: any;
+ callRef: string;
+ entity: string;
+ project: string;
+ readOnly?: boolean;
+ focused?: boolean;
+}
+
+// Utility function for creating feedback request
+const createFeedbackRequest = (
+ props: StructuredFeedbackProps,
+ value: any,
+ currentFeedbackId: string | null
+) => {
+ const baseRequest = {
+ project_id: `${props.entity}/${props.project}`,
+ weave_ref: props.callRef,
+ creator: null,
+ feedback_type: STRUCTURED_FEEDBACK_TYPE,
+ payload: {
+ value,
+ ref: props.sfData.ref,
+ name: props.sfData.name,
+ },
+ sort_by: [{created_at: 'desc'}],
+ };
+
+ if (currentFeedbackId) {
+ return {...baseRequest, feedback_id: currentFeedbackId};
+ }
+
+ return baseRequest;
+};
+
+const renderFeedbackComponent = (
+ props: StructuredFeedbackProps,
+ onAddFeedback: (value: any) => Promise,
+ foundValue: string | number | null,
+ currentFeedbackId: string | null
+) => {
+ switch (props.sfData.type) {
+ case FEEDBACK_TYPES.NUMERICAL:
+ return (
+
+ );
+ case FEEDBACK_TYPES.TEXT:
+ return (
+
+ );
+ case FEEDBACK_TYPES.CATEGORICAL:
+ return (
+
+ );
+ case FEEDBACK_TYPES.BOOLEAN:
+ return (
+
+ );
+ default:
+ return Unknown feedback type
;
+ }
+};
+
+export const StructuredFeedbackCell: React.FC<
+ StructuredFeedbackProps
+> = props => {
+ const {useFeedback} = useWFHooks();
+ const query = useFeedback({
+ entity: props.entity,
+ project: props.project,
+ weaveRef: props.callRef,
+ });
+
+ const [currentFeedbackId, setCurrentFeedbackId] = useState(
+ null
+ );
+ const [foundValue, setFoundValue] = useState(null);
+ const getTsClient = useGetTraceServerClientContext();
+
+ useEffect(() => {
+ if (!props.readOnly) {
+ // We don't need to listen for feedback changes if the cell is editable
+ return;
+ }
+ return getTsClient().registerOnFeedbackListener(
+ props.callRef,
+ query.refetch
+ );
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ useEffect(() => {
+ if (props.callRef !== query?.result?.[0]?.weave_ref) {
+ // The call was changed without the component unmounted, we need to reset
+ setFoundValue(null);
+ setCurrentFeedbackId(null);
+ }
+ }, [props.callRef, query?.result]);
+
+ const onAddFeedback = async (value: any): Promise => {
+ const tsClient = getTsClient();
+
+ if (!tsClient) {
+ console.error('Failed to get trace server client');
+ return false;
+ }
+
+ try {
+ let res: FeedbackCreateRes | FeedbackReplaceRes;
+
+ if (currentFeedbackId) {
+ const replaceRequest = createFeedbackRequest(
+ props,
+ value,
+ currentFeedbackId
+ ) as FeedbackReplaceReq;
+ res = await tsClient.feedbackReplace(replaceRequest);
+ } else {
+ const createRequest = createFeedbackRequest(
+ props,
+ value,
+ null
+ ) as FeedbackCreateReq;
+ res = await tsClient.feedbackCreate(createRequest);
+ }
+
+ if ('detail' in res) {
+ const errorRes = res as FeedbackCreateError;
+ console.error(
+ `Feedback ${currentFeedbackId ? 'replace' : 'create'} failed:`,
+ errorRes.detail
+ );
+ return false;
+ }
+ const successRes = res as FeedbackCreateSuccess;
+
+ if (successRes.id) {
+ setCurrentFeedbackId(successRes.id);
+ return true;
+ }
+
+ return false;
+ } catch (error) {
+ console.error(`Error in onAddFeedback:`, error);
+ return false;
+ }
+ };
+
+ useEffect(() => {
+ if (query?.loading) {
+ return;
+ }
+
+ // 3 conditions must be true for the feedback to be valid for this component:
+ // 1. Feedback is for this feedback spec
+ // 2. Feedback is for this structured feedback type
+ // 3. Feedback is for this structured feedback name
+
+ const feedbackTypeMatches = (feedback: any) =>
+ feedback.feedback_type === STRUCTURED_FEEDBACK_TYPE;
+ const feedbackNameMatches = (feedback: any) =>
+ feedback.payload.name === props.sfData.name;
+ const feedbackSpecMatches = (feedback: any) =>
+ feedback.payload.ref === props.sfData.ref;
+
+ const currFeedback = query.result?.find(
+ (feedback: any) =>
+ feedbackTypeMatches(feedback) &&
+ feedbackNameMatches(feedback) &&
+ feedbackSpecMatches(feedback)
+ );
+ if (!currFeedback) {
+ return;
+ }
+
+ setCurrentFeedbackId(currFeedback.id);
+ setFoundValue(currFeedback?.payload?.value ?? null);
+ }, [query?.result, query?.loading, props.sfData]);
+
+ if (query?.loading) {
+ return ;
+ }
+
+ if (props.readOnly) {
+ return {foundValue}
;
+ }
+
+ return (
+
+ {renderFeedbackComponent(
+ props,
+ onAddFeedback,
+ foundValue,
+ currentFeedbackId
+ )}
+
+ );
+};
+
+export const NumericalFeedbackColumn = ({
+ min,
+ max,
+ onAddFeedback,
+ defaultValue,
+ currentFeedbackId,
+ focused,
+}: {
+ min: number;
+ max: number;
+ onAddFeedback?: (value: number, currentFeedbackId: string | null) => Promise;
+ defaultValue: number | null;
+ currentFeedbackId?: string | null;
+ focused?: boolean;
+}) => {
+ const [value, setValue] = useState(
+ defaultValue ?? undefined
+ );
+ const [error, setError] = useState(false);
+
+ useEffect(() => {
+ setValue(defaultValue ?? undefined);
+ }, [defaultValue]);
+
+ const debouncedOnAddFeedback = useCallback(
+ debounce((val: number) => {
+ onAddFeedback?.(val, currentFeedbackId ?? null);
+ }, DEBOUNCE_VAL),
+ [onAddFeedback, currentFeedbackId]
+ );
+
+ const onValueChange = (v: string) => {
+ const val = parseInt(v);
+ setValue(val);
+ if (val < min || val > max) {
+ setError(true);
+ return;
+ } else {
+ setError(false);
+ }
+ debouncedOnAddFeedback(val);
+ };
+
+ return (
+
+
+ min: {min}, max: {max}
+
+
+
+ );
+};
+
+export const TextFeedbackColumn = ({
+ onAddFeedback,
+ defaultValue,
+ currentFeedbackId,
+ focused,
+}: {
+ onAddFeedback?: (
+ value: string,
+ currentFeedbackId: string | null
+ ) => Promise;
+ defaultValue: string | null;
+ currentFeedbackId?: string | null;
+ focused?: boolean;
+}) => {
+ const [value, setValue] = useState(defaultValue ?? '');
+
+ useEffect(() => {
+ setValue(defaultValue ?? '');
+ }, [defaultValue]);
+
+ const debouncedOnAddFeedback = useCallback(
+ debounce((val: string) => {
+ onAddFeedback?.(val, currentFeedbackId ?? null);
+ }, DEBOUNCE_VAL),
+ [onAddFeedback, currentFeedbackId]
+ );
+
+ const onValueChange = (newValue: string) => {
+ setValue(newValue);
+ debouncedOnAddFeedback(newValue);
+ };
+
+ return (
+
+
+
+ );
+};
+
+type Option = {
+ label: string;
+ value: string;
+};
+
+export const CategoricalFeedbackColumn = ({
+ options,
+ onAddFeedback,
+ defaultValue,
+ currentFeedbackId,
+ focused,
+}: {
+ options: string[];
+ onAddFeedback?: (
+ value: string,
+ currentFeedbackId: string | null
+ ) => Promise;
+ defaultValue: string | null;
+ currentFeedbackId?: string | null;
+ focused?: boolean;
+}) => {
+ const dropdownOptions = useMemo(() => {
+ const opts = options.map((option: string) => ({
+ label: option,
+ value: option,
+ }));
+ opts.splice(0, 0, {label: '', value: ''});
+ return opts;
+ }, [options]);
+ const [value, setValue] = useState
>
)}
diff --git a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CallsPage/CallsTableButtons.tsx b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CallsPage/CallsTableButtons.tsx
index 3718e696586..784f9790189 100644
--- a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CallsPage/CallsTableButtons.tsx
+++ b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CallsPage/CallsTableButtons.tsx
@@ -462,7 +462,7 @@ export const RefreshButton: FC<{
size="medium"
onClick={onClick}
tooltip="Refresh"
- icon="randomize-reset-reload"
+ icon="reload-refresh"
/>