diff --git a/weave-js/src/components/CodeEditor.tsx b/weave-js/src/components/CodeEditor.tsx index da97225ec60..1419c79f301 100644 --- a/weave-js/src/components/CodeEditor.tsx +++ b/weave-js/src/components/CodeEditor.tsx @@ -5,7 +5,7 @@ * the text content. */ -import {MOON_250} from '@wandb/weave/common/css/globals.styles'; +import {MOON_250, MOON_500} from '@wandb/weave/common/css/globals.styles'; import React, {useRef, useState} from 'react'; const Editor = React.lazy(async () => { @@ -22,6 +22,7 @@ type CodeEditorProps = { onChange?: (value: string) => void; maxHeight?: number; minHeight?: number; + placeholder?: string; // To enable horizontal scrolling with the mouse wheel and not just the scrollbar, // set handleMouseWheel to true and alwaysConsumeMouseWheel to false. diff --git a/weave-js/src/components/FancyPage/useProjectSidebar.ts b/weave-js/src/components/FancyPage/useProjectSidebar.ts index ab3e11a77df..6d7e5439903 100644 --- a/weave-js/src/components/FancyPage/useProjectSidebar.ts +++ b/weave-js/src/components/FancyPage/useProjectSidebar.ts @@ -186,6 +186,13 @@ export const useProjectSidebar = ( isShown: isWeaveOnly, iconName: IconNames.CubeContainer, }, + { + type: 'button' as const, + name: 'Playground', + slug: 'weave/playground', + isShown: isWeaveOnly, + iconName: IconNames.CubeContainer, + }, { type: 'menuPlaceholder' as const, // name: 'More', diff --git a/weave-js/src/components/PagePanelComponents/Home/Browse3.tsx b/weave-js/src/components/PagePanelComponents/Home/Browse3.tsx index bee4705042c..46bf8956a97 100644 --- a/weave-js/src/components/PagePanelComponents/Home/Browse3.tsx +++ b/weave-js/src/components/PagePanelComponents/Home/Browse3.tsx @@ -92,6 +92,7 @@ import {OpPage} from './Browse3/pages/OpPage'; import {OpsPage} from './Browse3/pages/OpsPage'; import {OpVersionPage} from './Browse3/pages/OpVersionPage'; import {OpVersionsPage} from './Browse3/pages/OpVersionsPage'; +import {PlaygroundPage} from './Browse3/pages/PlaygroundPage/PlaygroundPage'; import {TablePage} from './Browse3/pages/TablePage'; import {TablesPage} from './Browse3/pages/TablesPage'; import {useURLSearchParamsDict} from './Browse3/pages/util'; @@ -486,6 +487,13 @@ const Browse3ProjectRoot: FC<{ + + + @@ -652,7 +660,17 @@ const useParamsDecoded = () => { }, [params]); }; -// TODO(tim/weaveflow_improved_nav): Generalize this +const PlaygroundPageBinding = () => { + const params = useParamsDecoded(); + return ( + + ); +}; + const CallPageBinding = () => { useCallPeekRedirect(); const params = useParamsDecoded(); diff --git a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CallPage/CallPage.tsx b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CallPage/CallPage.tsx index 79f091e6a31..36a15a287de 100644 --- a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CallPage/CallPage.tsx +++ b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CallPage/CallPage.tsx @@ -1,5 +1,6 @@ import Box from '@mui/material/Box'; import {Loading} from '@wandb/weave/components/Loading'; +import {urlPrefixed} from '@wandb/weave/config'; import {useViewTraceEvent} from '@wandb/weave/integrations/analytics/useViewEvents'; import React, {FC, useCallback, useEffect, useState} from 'react'; import {useHistory} from 'react-router-dom'; @@ -11,6 +12,7 @@ import {Browse2OpDefCode} from '../../../Browse2/Browse2OpDefCode'; import {TRACETREE_PARAM, useWeaveflowCurrentRouteContext} from '../../context'; import {FeedbackGrid} from '../../feedback/FeedbackGrid'; import {NotFoundPanel} from '../../NotFoundPanel'; +import {CallChat} from '../ChatView/CallChat'; import {isCallChat} from '../ChatView/hooks'; import {isEvaluateOp} from '../common/heuristics'; import {CenteredAnimatedLoader} from '../common/Loader'; @@ -23,7 +25,6 @@ import {TabUseCall} from '../TabUseCall'; import {useURLSearchParamsDict} from '../util'; import {useWFHooks} from '../wfReactInterface/context'; import {CallSchema} from '../wfReactInterface/wfDataModelHooksInterface'; -import {CallChat} from './CallChat'; import {CallDetails} from './CallDetails'; import {CallOverview} from './CallOverview'; import {CallSummary} from './CallSummary'; @@ -54,6 +55,14 @@ const useCallTabs = (call: CallSchema) => { const codeURI = call.opVersionRef; const {entity, project, callId} = call; const weaveRef = makeRefCall(entity, project, callId); + + const handleOpenInPlayground = () => { + window.open( + urlPrefixed(`/${entity}/${project}/weave/playground/${callId}`), + '_blank' + ); + }; + return [ // Disabling Evaluation tab until it's better for single evaluation ...(false && isEvaluateOp(call.spanName) @@ -80,11 +89,20 @@ const useCallTabs = (call: CallSchema) => { { label: 'Chat', content: ( - - - - - + <> + + + + + + + ), }, ] diff --git a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/ChatView/CallChat.tsx b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/ChatView/CallChat.tsx new file mode 100644 index 00000000000..7f597536ee8 --- /dev/null +++ b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/ChatView/CallChat.tsx @@ -0,0 +1,62 @@ +/** + * Get normalized version of call data in chat format and display it. + */ + +import React, {useEffect, useState} from 'react'; + +import {LoadingDots} from '../../../../../LoadingDots'; +import {PlaygroundContext} from '../PlaygroundPage/PlaygroundChat/PlaygroundContext'; +import {TraceCallSchema} from '../wfReactInterface/traceServerClientTypes'; +import {ChatView} from './ChatView'; +import {useCallAsChat} from './hooks'; + +type CallChatProps = { + call: TraceCallSchema; + isPlayground?: boolean; + deleteMessage?: (messageIndex: number) => void; + editMessage?: (messageIndex: number, newMessage: any) => void; + deleteChoice?: (choiceIndex: number) => void; + addMessage?: (newMessage: any) => void; + editChoice?: (choiceIndex: number, newChoice: any) => void; + retry?: (messageIndex: number, isChoice?: boolean) => void; +}; + +export const CallChat = ({ + call, + isPlayground = false, + deleteMessage, + editMessage, + deleteChoice, + addMessage, + editChoice, + retry, +}: CallChatProps) => { + const chat = useCallAsChat(call); + + // This is used because when we first load the chat view in a drawer, the animation cant handle all the rows + // so we delay for the first render + const [animationBuffer, setAnimationBuffer] = useState(true); + useEffect(() => { + setTimeout(() => { + setAnimationBuffer(false); + }, 300); + }, []); + + if (chat.loading || animationBuffer) { + return ; + } + return ( + + + + ); +}; diff --git a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/ChatView/ChatView.tsx b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/ChatView/ChatView.tsx index 9109a88fc92..fce954bb552 100644 --- a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/ChatView/ChatView.tsx +++ b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/ChatView/ChatView.tsx @@ -1,20 +1,26 @@ -import React, {useEffect, useRef} from 'react'; +import React, {useEffect, useRef, useMemo} from 'react'; import {useDeepMemo} from '../../../../../../hookUtils'; +import {TraceCallSchema} from '../wfReactInterface/traceServerClientTypes'; import {ChoicesView} from './ChoicesView'; -import {HorizontalRuleWithLabel} from './HorizontalRuleWithLabel'; import {MessageList} from './MessageList'; import {Chat} from './types'; type ChatViewProps = { + call?: TraceCallSchema; chat: Chat; }; -export const ChatView = ({chat}: ChatViewProps) => { +export const ChatView = ({call, chat}: ChatViewProps) => { const outputRef = useRef(null); const chatResult = useDeepMemo(chat.result); + const scrollLastMessage = useMemo( + () => !(outputRef.current && chatResult && chatResult.choices), + [outputRef.current, chatResult, chatResult?.choices] + ); + useEffect(() => { if (outputRef.current && chatResult && chatResult.choices) { outputRef.current.scrollIntoView(); @@ -23,11 +29,12 @@ export const ChatView = ({chat}: ChatViewProps) => { return (
- - + {chatResult && chatResult.choices && ( -
- +
{ const {message} = choice; return ( - + ); }; diff --git a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/ChatView/MessageList.tsx b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/ChatView/MessageList.tsx index c0e964f2550..b90115bf6c3 100644 --- a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/ChatView/MessageList.tsx +++ b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/ChatView/MessageList.tsx @@ -1,17 +1,68 @@ -import React from 'react'; +import React, {useEffect, useRef} from 'react'; import {MessagePanel} from './MessagePanel'; -import {Messages} from './types'; +import {Message, Messages} from './types'; type MessageListProps = { messages: Messages; + scrollLastMessage?: boolean; }; -export const MessageList = ({messages}: MessageListProps) => { +export const MessageList = ({ + messages, + scrollLastMessage = false, +}: MessageListProps) => { + const lastMessageRef = useRef(null); + const processedMessages = []; + + // This is ugly will refactor, associates tool calls with their responses + for (let i = 0; i < messages.length; i++) { + const message = messages[i]; + if (!message.tool_calls) { + processedMessages.push(message); + continue; + } + const toolCalls = message.tool_calls!; + + const toolMessages = []; + // Get next messages where role = tool + while (i + 1 < messages.length && messages[i + 1].role === 'tool') { + toolMessages.push(messages[i + 1]); + i++; + } + for (let j = 0; j < toolCalls.length; j++) { + let response: Message | undefined; + for (const toolMessage of toolMessages) { + if (toolMessage.tool_call_id === toolCalls[j].id) { + response = toolMessage; + break; + } + } + toolCalls[j] = { + ...toolCalls[j], + response, + }; + } + processedMessages.push({ + ...message, + tool_call: toolCalls, + }); + } + + useEffect(() => { + if (lastMessageRef.current && scrollLastMessage) { + lastMessageRef.current.scrollIntoView(); + } + }, [messages.length, scrollLastMessage, lastMessageRef.current]); + return ( -
- {messages.map((m, i) => ( - +
+ {processedMessages.map((m, i) => ( +
+ +
))}
); diff --git a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/ChatView/MessagePanel.tsx b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/ChatView/MessagePanel.tsx index 4ee315b8842..653640d4847 100644 --- a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/ChatView/MessagePanel.tsx +++ b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/ChatView/MessagePanel.tsx @@ -1,40 +1,263 @@ +import {Button} from '@wandb/weave/components/Button'; +import {Callout} from '@wandb/weave/components/Callout'; import classNames from 'classnames'; import _ from 'lodash'; -import React from 'react'; +import React, {useEffect, useRef, useState} from 'react'; +import {usePlaygroundContext} from '../PlaygroundPage/PlaygroundChat/PlaygroundContext'; +import {TextArea} from '../PlaygroundPage/Textarea'; import {MessagePanelPart} from './MessagePanelPart'; +import {ShowMoreButton} from './ShowMoreButton'; import {ToolCalls} from './ToolCalls'; import {Message} from './types'; type MessagePanelProps = { + index: number; message: Message; isStructuredOutput?: boolean; + isChoice?: boolean; + isNested?: boolean; + isAddingToolResponse?: boolean; }; export const MessagePanel = ({ + index, message, isStructuredOutput, + isChoice, + isNested, + isAddingToolResponse = false, }: MessagePanelProps) => { + const [isShowingMore, setIsShowingMore] = useState(false); + const [isOverflowing, setIsOverflowing] = useState(false); + const [isHovering, setIsHovering] = useState(false); + const [editorHeight, setEditorHeight] = useState( + isAddingToolResponse ? 100 : null + ); + const contentRef = useRef(null); + + const { + isPlayground, + deleteMessage, + editMessage, + deleteChoice, + editChoice, + retry, + } = usePlaygroundContext(); + useEffect(() => { + if (contentRef.current) { + setIsOverflowing(contentRef.current.scrollHeight > 400); + } + }, [message.content]); + const isUser = message.role === 'user'; - const bg = isUser ? 'bg-cactus-300/[0.48]' : 'bg-moon-100'; + const isSystemPrompt = message.role === 'system'; + const isTool = message.role === 'tool'; + const hasToolCalls = message.tool_calls && message.tool_calls.length > 0; + const hasContent = message.content && message.content.length > 0; + + const bg = isUser ? 'bg-cactus-300/[0.24]' : ''; + const border = isSystemPrompt ? 'border border-moon-250 rounded-lg' : ''; + const justification = isUser ? 'ml-auto' : 'mr-auto'; + const maxHeight = isShowingMore ? 'max-h-full' : 'max-h-[400px]'; + const maxWidth = isSystemPrompt || isNested ? 'w-full' : 'max-w-3xl'; + const toolWidth = + isTool || hasToolCalls || isStructuredOutput || editorHeight ? 'w-3/4' : ''; + + const capitalizedRole = + message.role.charAt(0).toUpperCase() + message.role.slice(1); + + const [editedContent, setEditedContent] = useState( + _.isString(message.content) ? message.content : message.content?.join('') + ); + + // Add this useEffect hook + useEffect(() => { + setEditedContent( + _.isString(message.content) ? message.content : message.content?.join('') + ); + }, [message.content]); + + const handleSave = () => { + if (isChoice) { + editChoice?.(index, { + message: {content: editedContent}, + }); + } else { + editMessage?.(index, { + ...message, + content: editedContent, + }); + } + setEditorHeight(null); + }; + + const handleCancel = () => { + setEditedContent( + _.isString(message.content) ? message.content : message.content?.join('') + ); + setEditorHeight(null); + }; + return ( -
-
{message.role}
- {message.content && ( -
- {_.isString(message.content) ? ( - + {!isNested && ( +
+ {!isUser && !isTool && ( + - ) : ( - message.content.map((p, i) => ( - - )) )}
)} - {message.tool_calls && } + +
setIsHovering(true)} + onMouseLeave={() => setIsHovering(false)}> +
+ {isSystemPrompt && ( +
+
{capitalizedRole}
+
+ )} + + {isTool && ( +
+
+ Response +
+
+ )} + +
+ {editorHeight ? ( +
+