Spaces:
Running
Running
/** | |
* Copyright 2024 Google LLC | |
* | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
*/ | |
import "./logger.scss"; | |
import { Part } from "@google/generative-ai"; | |
import cn from "classnames"; | |
import { ReactNode } from "react"; | |
import { useLoggerStore } from "../../lib/store-logger"; | |
import SyntaxHighlighter from "react-syntax-highlighter"; | |
import { vs2015 as dark } from "react-syntax-highlighter/dist/esm/styles/hljs"; | |
import { | |
ClientContentMessage, | |
isClientContentMessage, | |
isInterrupted, | |
isModelTurn, | |
isServerContentMessage, | |
isToolCallCancellationMessage, | |
isToolCallMessage, | |
isToolResponseMessage, | |
isTurnComplete, | |
ModelTurn, | |
ServerContentMessage, | |
StreamingLog, | |
ToolCallCancellationMessage, | |
ToolCallMessage, | |
ToolResponseMessage, | |
} from "../../multimodal-live-types"; | |
const formatTime = (d: Date) => d.toLocaleTimeString().slice(0, -3); | |
const LogEntry = ({ | |
log, | |
MessageComponent, | |
}: { | |
log: StreamingLog; | |
MessageComponent: ({ | |
message, | |
}: { | |
message: StreamingLog["message"]; | |
}) => ReactNode; | |
}): JSX.Element => ( | |
<li | |
className={cn( | |
`plain-log`, | |
`source-${log.type.slice(0, log.type.indexOf("."))}`, | |
{ | |
receive: log.type.includes("receive"), | |
send: log.type.includes("send"), | |
}, | |
)} | |
> | |
<span className="timestamp">{formatTime(log.date)}</span> | |
<span className="source">{log.type}</span> | |
<span className="message"> | |
<MessageComponent message={log.message} /> | |
</span> | |
{log.count && <span className="count">{log.count}</span>} | |
</li> | |
); | |
const PlainTextMessage = ({ | |
message, | |
}: { | |
message: StreamingLog["message"]; | |
}) => <span>{message as string}</span>; | |
type Message = { message: StreamingLog["message"] }; | |
const AnyMessage = ({ message }: Message) => ( | |
<pre>{JSON.stringify(message, null, " ")}</pre> | |
); | |
function tryParseCodeExecutionResult(output: string) { | |
try { | |
const json = JSON.parse(output); | |
return JSON.stringify(json, null, " "); | |
} catch (e) { | |
return output; | |
} | |
} | |
const RenderPart = ({ part }: { part: Part }) => | |
part.text && part.text.length ? ( | |
<p className="part part-text">{part.text}</p> | |
) : part.executableCode ? ( | |
<div className="part part-executableCode"> | |
<h5>executableCode: {part.executableCode.language}</h5> | |
<SyntaxHighlighter | |
language={part.executableCode.language.toLowerCase()} | |
style={dark} | |
> | |
{part.executableCode.code} | |
</SyntaxHighlighter> | |
</div> | |
) : part.codeExecutionResult ? ( | |
<div className="part part-codeExecutionResult"> | |
<h5>codeExecutionResult: {part.codeExecutionResult.outcome}</h5> | |
<SyntaxHighlighter language="json" style={dark}> | |
{tryParseCodeExecutionResult(part.codeExecutionResult.output)} | |
</SyntaxHighlighter> | |
</div> | |
) : ( | |
<div className="part part-inlinedata"> | |
<h5>Inline Data: {part.inlineData?.mimeType}</h5> | |
</div> | |
); | |
const ClientContentLog = ({ message }: Message) => { | |
const { turns, turnComplete } = (message as ClientContentMessage) | |
.clientContent; | |
return ( | |
<div className="rich-log client-content user"> | |
<h4 className="roler-user">User</h4> | |
{turns.map((turn, i) => ( | |
<div key={`message-turn-${i}`}> | |
{turn.parts | |
.filter((part) => !(part.text && part.text === "\n")) | |
.map((part, j) => ( | |
<RenderPart part={part} key={`message-turh-${i}-part-${j}`} /> | |
))} | |
</div> | |
))} | |
{!turnComplete ? <span>turnComplete: false</span> : ""} | |
</div> | |
); | |
}; | |
const ToolCallLog = ({ message }: Message) => { | |
const { toolCall } = message as ToolCallMessage; | |
return ( | |
<div className={cn("rich-log tool-call")}> | |
{toolCall.functionCalls.map((fc, i) => ( | |
<div key={fc.id} className="part part-functioncall"> | |
<h5>Function call: {fc.name}</h5> | |
<SyntaxHighlighter language="json" style={dark}> | |
{JSON.stringify(fc, null, " ")} | |
</SyntaxHighlighter> | |
</div> | |
))} | |
</div> | |
); | |
}; | |
const ToolCallCancellationLog = ({ message }: Message): JSX.Element => ( | |
<div className={cn("rich-log tool-call-cancellation")}> | |
<span> | |
{" "} | |
ids:{" "} | |
{(message as ToolCallCancellationMessage).toolCallCancellation.ids.map( | |
(id) => ( | |
<span className="inline-code" key={`cancel-${id}`}> | |
"{id}" | |
</span> | |
), | |
)} | |
</span> | |
</div> | |
); | |
const ToolResponseLog = ({ message }: Message): JSX.Element => ( | |
<div className={cn("rich-log tool-response")}> | |
{(message as ToolResponseMessage).toolResponse.functionResponses.map( | |
(fc) => ( | |
<div key={`tool-response-${fc.id}`} className="part"> | |
<h5>Function Response: {fc.id}</h5> | |
<SyntaxHighlighter language="json" style={dark}> | |
{JSON.stringify(fc.response, null, " ")} | |
</SyntaxHighlighter> | |
</div> | |
), | |
)} | |
</div> | |
); | |
const ModelTurnLog = ({ message }: Message): JSX.Element => { | |
const serverContent = (message as ServerContentMessage).serverContent; | |
const { modelTurn } = serverContent as ModelTurn; | |
const { parts } = modelTurn; | |
return ( | |
<div className="rich-log model-turn model"> | |
<h4 className="role-model">Model</h4> | |
{parts | |
.filter((part) => !(part.text && part.text === "\n")) | |
.map((part, j) => ( | |
<RenderPart part={part} key={`model-turn-part-${j}`} /> | |
))} | |
</div> | |
); | |
}; | |
const CustomPlainTextLog = (msg: string) => () => ( | |
<PlainTextMessage message={msg} /> | |
); | |
export type LoggerFilterType = "conversations" | "tools" | "none"; | |
export type LoggerProps = { | |
filter: LoggerFilterType; | |
}; | |
const filters: Record<LoggerFilterType, (log: StreamingLog) => boolean> = { | |
tools: (log: StreamingLog) => | |
isToolCallMessage(log.message) || | |
isToolResponseMessage(log.message) || | |
isToolCallCancellationMessage(log.message), | |
conversations: (log: StreamingLog) => | |
isClientContentMessage(log.message) || isServerContentMessage(log.message), | |
none: () => true, | |
}; | |
const component = (log: StreamingLog) => { | |
if (typeof log.message === "string") { | |
return PlainTextMessage; | |
} | |
if (isClientContentMessage(log.message)) { | |
return ClientContentLog; | |
} | |
if (isToolCallMessage(log.message)) { | |
return ToolCallLog; | |
} | |
if (isToolCallCancellationMessage(log.message)) { | |
return ToolCallCancellationLog; | |
} | |
if (isToolResponseMessage(log.message)) { | |
return ToolResponseLog; | |
} | |
if (isServerContentMessage(log.message)) { | |
const { serverContent } = log.message; | |
if (isInterrupted(serverContent)) { | |
return CustomPlainTextLog("interrupted"); | |
} | |
if (isTurnComplete(serverContent)) { | |
return CustomPlainTextLog("turnComplete"); | |
} | |
if (isModelTurn(serverContent)) { | |
return ModelTurnLog; | |
} | |
} | |
return AnyMessage; | |
}; | |
export default function Logger({ filter = "none" }: LoggerProps) { | |
const { logs } = useLoggerStore(); | |
const filterFn = filters[filter]; | |
return ( | |
<div className="logger"> | |
<ul className="logger-list"> | |
{logs.filter(filterFn).map((log, key) => { | |
return ( | |
<LogEntry MessageComponent={component(log)} log={log} key={key} /> | |
); | |
})} | |
</ul> | |
</div> | |
); | |
} | |