diff --git a/components/chat/messages/calls.tsx b/components/chat/messages/calls.tsx index fcf39667..18d9fe46 100644 --- a/components/chat/messages/calls.tsx +++ b/components/chat/messages/calls.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import StackTrace from './calls/stackTrace'; +import CallFrames from './calls/callFrames'; import type { CallFrame } from '@gptscript-ai/gptscript'; import { IoCloseSharp } from 'react-icons/io5'; import { BsArrowsFullscreen } from 'react-icons/bs'; @@ -43,7 +43,7 @@ const Calls = ({ calls }: { calls: Record }) => {
-

Stack Trace

+

Call Frames

@@ -77,7 +77,7 @@ const Calls = ({ calls }: { calls: Record }) => {

- + diff --git a/components/chat/messages/calls/callFrames.tsx b/components/chat/messages/calls/callFrames.tsx new file mode 100644 index 00000000..44ff2406 --- /dev/null +++ b/components/chat/messages/calls/callFrames.tsx @@ -0,0 +1,348 @@ +import React, { useState, useRef } from 'react'; +import { Button, Tooltip } from '@nextui-org/react'; +import type { CallFrame } from '@gptscript-ai/gptscript'; +import { FaChevronUp, FaChevronDown } from 'react-icons/fa'; +import { AiOutlineLoading3Quarters } from 'react-icons/ai'; +import { JSONTree } from 'react-json-tree'; + +const CallFrames = ({ calls }: { calls: Record | null }) => { + // TODO Move this to before the final return so that we aren't disobeying the rules of hooks + if (!calls) return null; + + // eslint-disable-next-line react-hooks/rules-of-hooks + const logsContainerRef = useRef(null); + // eslint-disable-next-line react-hooks/rules-of-hooks + const [allOpen, setAllOpen] = useState(false); + + const EmptyLogs = () => { + return ( +
+

Waiting for the first event from GPTScript...

+
+ ); + }; + + // Build tree structure + const buildTree = (calls: Record) => { + const tree: Record = {}; + const rootNodes: string[] = []; + + // Sort calls by start timestamp + const sortedCalls = Object.entries(calls).sort( + (a, b) => new Date(a[1].start).getTime() - new Date(b[1].start).getTime() + ); + + sortedCalls.forEach(([id, call]) => { + // Skip "GPTScript Gateway Provider" calls + if (call.tool?.name === 'GPTScript Gateway Provider') { + return; + } + + const parentId = call.parentID || ''; + if (!parentId) { + rootNodes.push(id); + } else { + if (!tree[parentId]) { + tree[parentId] = []; + } + tree[parentId].push(id); + } + }); + + return { tree, rootNodes }; + }; + + // Render input (JSON or text) + const renderInput = (input: any) => { + if (typeof input === 'string') { + try { + const jsonInput = JSON.parse(input); + return ( + !allOpen} + /> + ); + } catch (_) { + // If parsing fails, render as text + return

{input}

; + } + } + // If input is already an object, render as JSON + return ( + !allOpen} + /> + ); + }; + + // Helper function to truncate and stringify input + const truncateInput = (input: any): string => { + const stringified = + typeof input === 'string' ? input : JSON.stringify(input); + return stringified.length > 100 + ? stringified.slice(0, 100) + '...' + : stringified; + }; + + // Render tree recursively + const renderTree = (nodeId: string, depth: number = 0) => { + const call = calls[nodeId]; + const children = tree[nodeId] || []; + + return ( +
+
+ +
+ {call.tool?.source?.location && + call.tool.source.location !== 'inline' && ( + + )} +
+ + Input Message: {truncateInput(call?.input)} + +
{renderInput(call?.input)}
+
+
+ Output Messages +
    + {call.output && call.output.length > 0 ? ( + call.output.flatMap((output, key) => { + if (output.content) { + return [ +
  • +
    + + {truncateInput(output.content)} + +

    + {output.content} +

    +
    +
  • , + ]; + } else if (output.subCalls) { + return Object.entries(output.subCalls).map( + ([subCallKey, subCall]) => ( +
  • +
    + + Tool call: {truncateInput(subCallKey)} + +

    + Tool Call ID: {subCallKey} +

    +

    + Tool ID: {subCall.toolID} +

    +

    + Input: {subCall.input} +

    +
    +
  • + ) + ); + } + return []; + }) + ) : ( +
  • +

    No output available

    +
  • + )} +
+
+ {children.length > 0 && ( +
+ Subcalls +
+ {children.map((childId: string) => + renderTree(childId, depth + 1) + )} +
+
+ )} + {(call.llmRequest || call.llmResponse) && ( +
+ + {call.llmRequest && 'messages' in call.llmRequest + ? 'LLM Request & Response' + : 'Tool Command and Output'} + +
+ {call.llmRequest && ( +
+ + {call.llmRequest && 'messages' in call.llmRequest + ? 'Request' + : 'Command'} + +
{renderInput(call.llmRequest)}
+
+ )} + {call.llmResponse && ( +
+ + {call.llmRequest && 'messages' in call.llmRequest + ? 'Response' + : 'Output'} + +
+ {renderInput(call.llmResponse)} +
+
+ )} +
+
+ )} + {call.tool?.toolMapping && ( +
+ Tools +
+ {renderToolMapping(call.tool.toolMapping)} +
+
+ )} + {call.tool?.export && ( +
+ Shared Tools +
{renderExports(call.tool.export)}
+
+ )} +
+
+
+ ); + }; + + const renderToolMapping = (toolMapping: Record) => { + return Object.entries(toolMapping).map(([key, value]) => ( +
+ {value.some((item: any) => item.toolID !== key) ? ( + <> + {key}: +
    + {value.map((item: any, index: number) => ( +
  • +

    {item.reference}

    +

    {item.toolID}

    +
  • + ))} +
+ + ) : ( +

{key}

+ )} +
+ )); + }; + + const renderExports = (exports: string[]) => { + return ( +
    + {exports.map((item, index) => ( +
  • +

    {item}

    +
  • + ))} +
+ ); + }; + + const { tree, rootNodes } = buildTree(calls); + + return ( +
+ + + + {rootNodes.length > 0 ? ( + rootNodes.map((rootId) => renderTree(rootId)) + ) : ( + + )} +
+ ); +}; + +const Summary = ({ call }: { call: CallFrame }) => { + const name = + call.tool?.name || + call.tool?.source?.repo || + call.tool?.source?.location || + 'main'; + const category = call.toolCategory; + + const startTime = new Date(call.start).toLocaleTimeString(); + const endTime = call.end + ? new Date(call.end).toLocaleTimeString() + : 'In progress'; + const duration = call.end + ? `${((new Date(call.end).getTime() - new Date(call.start).getTime()) / 1000).toFixed(2)}s` + : 'N/A'; + + const timeInfo = `[ID: ${call.id}] [${startTime} - ${endTime}, ${duration}]`; + + let summaryContent; + if (call.tool?.chat) { + summaryContent = + call.type !== 'callFinish' + ? `Chat open with ${name}` + : `Chatted with ${name}`; + } else { + summaryContent = + call.type !== 'callFinish' + ? category + ? `Loading ${category} from ${name}` + '...' + : `Running ${name}` + : category + ? `Loaded ${category} from ${name}` + : `Ran ${name}`; + } + + return ( + + {summaryContent} {timeInfo} + {call?.type !== 'callFinish' && ( + + )} + + ); +}; + +export default CallFrames; diff --git a/components/chat/messages/calls/stackTrace.tsx b/components/chat/messages/calls/stackTrace.tsx deleted file mode 100644 index abcb6d61..00000000 --- a/components/chat/messages/calls/stackTrace.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import React, { useRef, useState } from 'react'; -import { Button, Tooltip } from '@nextui-org/react'; -import type { CallFrame } from '@gptscript-ai/gptscript'; -import { GoArrowDown, GoArrowUp } from 'react-icons/go'; -import { AiOutlineLoading3Quarters } from 'react-icons/ai'; - -const StackTrace = ({ calls }: { calls: Record | null }) => { - if (!calls) return null; - - // eslint-disable-next-line react-hooks/rules-of-hooks - const logsContainerRef = useRef(null); - // eslint-disable-next-line react-hooks/rules-of-hooks - const [allOpen, setAllOpen] = useState(true); - - const EmptyLogs = () => { - return ( -
-

Waiting for the first event from GPTScript...

-
- ); - }; - - const Summary = ({ call }: { call: CallFrame }) => { - const name = - call.tool?.name || - call.tool?.source?.repo || - call.tool?.source?.location || - 'main'; - const category = call.toolCategory; - if (call.tool?.chat) { - return ( - - {call.type !== 'callFinish' - ? `Chat open with ${name}` - : `Chatted with ${name}`} - - ); - } - - return ( - - {call.type !== 'callFinish' - ? category - ? `Loading ${category} from ${name}` + '...' - : `Running ${name}` - : category - ? `Loaded ${category} from ${name}` - : `Ran ${name}`} - {call?.type !== 'callFinish' && ( - - )} - - ); - }; - - const RenderLogs = () => { - const callMap = new Map>(); - - Object.keys(calls).forEach((key) => { - const parentId = calls[key].parentID || ''; - const callFrame = callMap.get(parentId); - if (!callFrame) { - callMap.set(parentId, new Map()); - } - callMap.get(parentId)?.set(key, calls[key]); - }); - - const renderLogsRecursive = (parentId: string) => { - const logs = callMap.get(parentId); - if (!logs) return null; - return ( -
- {Array.from(logs.entries()).map(([key, call]) => ( -
-
- -
-
- Input -

{JSON.stringify(call?.input)}

-
-
- Messages - -
-
    - {call.output && - call.output.map( - (output, key) => - output.content && ( -
  • -

    {output.content && output.content}

    -
  • - ) - )} -
-
-
- {callMap.get(call.id) && ( -
- Calls - {renderLogsRecursive(call.id)} -
- )} -
-
-
- ))} -
- ); - }; - - return renderLogsRecursive(''); - }; - - return ( -
- - - - {calls ? : } -
- ); -}; - -export default StackTrace; diff --git a/components/saveFile.tsx b/components/saveFile.tsx index e6fc97e9..aebd325b 100644 --- a/components/saveFile.tsx +++ b/components/saveFile.tsx @@ -13,7 +13,7 @@ const SaveFile: React.FC = ({ content, className }) => { }; return ( - +