Trudy's picture
Initial commit: Gemini Realtime Console
7f2a14a
/**
* 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>
);
}