mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-27 03:38:39 -05:00
Show tool calls separately from message
This commit is contained in:
@@ -4,5 +4,7 @@
|
||||
"processing": "Processing...",
|
||||
"toolsUsed": "Used: {{tools}}",
|
||||
"showTools": "Show tools ({{count}})",
|
||||
"hideTools": "Hide tools"
|
||||
"hideTools": "Hide tools",
|
||||
"call": "Call",
|
||||
"result": "Result"
|
||||
}
|
||||
|
||||
@@ -1,66 +1,9 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
|
||||
export type ToolCall = {
|
||||
name: string;
|
||||
arguments?: Record<string, unknown>;
|
||||
response?: string;
|
||||
};
|
||||
|
||||
type AssistantMessageProps = {
|
||||
content: string;
|
||||
toolCalls?: ToolCall[];
|
||||
};
|
||||
|
||||
export function AssistantMessage({
|
||||
content,
|
||||
toolCalls,
|
||||
}: AssistantMessageProps) {
|
||||
const { t } = useTranslation(["views/chat"]);
|
||||
const [open, setOpen] = useState(false);
|
||||
const hasToolCalls = toolCalls && toolCalls.length > 0;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<ReactMarkdown>{content}</ReactMarkdown>
|
||||
{hasToolCalls && (
|
||||
<Collapsible open={open} onOpenChange={setOpen}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-auto py-1 text-xs text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{open
|
||||
? t("hideTools")
|
||||
: t("showTools", { count: toolCalls.length })}
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<ul className="mt-2 space-y-2 border-l-2 border-muted-foreground/30 pl-3">
|
||||
{toolCalls.map((tc, idx) => (
|
||||
<li key={idx} className="text-xs">
|
||||
<span className="font-medium text-muted-foreground">
|
||||
{tc.name}
|
||||
</span>
|
||||
{tc.response != null && tc.response !== "" && (
|
||||
<pre className="mt-1 max-h-32 overflow-auto rounded bg-muted/50 p-2 text-[10px]">
|
||||
{tc.response}
|
||||
</pre>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
export function AssistantMessage({ content }: AssistantMessageProps) {
|
||||
return <ReactMarkdown>{content}</ReactMarkdown>;
|
||||
}
|
||||
|
||||
77
web/src/components/chat/ToolCallBubble.tsx
Normal file
77
web/src/components/chat/ToolCallBubble.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ChevronDown, ChevronRight } from "lucide-react";
|
||||
|
||||
type ToolCallBubbleProps = {
|
||||
name: string;
|
||||
arguments?: Record<string, unknown>;
|
||||
response?: string;
|
||||
side: "left" | "right";
|
||||
};
|
||||
|
||||
export function ToolCallBubble({
|
||||
name,
|
||||
arguments: args,
|
||||
response,
|
||||
side,
|
||||
}: ToolCallBubbleProps) {
|
||||
const { t } = useTranslation(["views/chat"]);
|
||||
const [open, setOpen] = useState(false);
|
||||
const isLeft = side === "left";
|
||||
const normalizedName = name
|
||||
.replace(/_/g, " ")
|
||||
.split(" ")
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||
.join(" ");
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
isLeft
|
||||
? "self-start rounded-lg bg-muted px-3 py-2"
|
||||
: "self-end rounded-lg bg-primary px-3 py-2 text-primary-foreground"
|
||||
}
|
||||
>
|
||||
<Collapsible open={open} onOpenChange={setOpen}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-auto w-full justify-start gap-2 p-0 text-xs hover:bg-transparent"
|
||||
>
|
||||
{open ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
|
||||
<span className="font-medium">
|
||||
{isLeft ? t("call") : t("result")} {normalizedName}
|
||||
</span>
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="mt-2 space-y-2">
|
||||
{isLeft && args && Object.keys(args).length > 0 && (
|
||||
<div className="text-xs">
|
||||
<div className="font-medium text-muted-foreground">Arguments:</div>
|
||||
<pre className="mt-1 max-h-32 overflow-auto rounded bg-muted/50 p-2 text-[10px]">
|
||||
{JSON.stringify(args, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
{!isLeft && response && response !== "" && (
|
||||
<div className="text-xs">
|
||||
<div className="font-medium opacity-80">Response:</div>
|
||||
<pre className="mt-1 max-h-32 overflow-auto rounded bg-primary/20 p-2 text-[10px]">
|
||||
{response}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -4,16 +4,9 @@ import { FaArrowUpLong } from "react-icons/fa6";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useState, useCallback } from "react";
|
||||
import axios from "axios";
|
||||
import {
|
||||
AssistantMessage,
|
||||
type ToolCall,
|
||||
} from "@/components/chat/AssistantMessage";
|
||||
|
||||
type ChatMessage = {
|
||||
role: "user" | "assistant";
|
||||
content: string;
|
||||
toolCalls?: ToolCall[];
|
||||
};
|
||||
import { AssistantMessage } from "@/components/chat/AssistantMessage";
|
||||
import { ToolCallBubble } from "@/components/chat/ToolCallBubble";
|
||||
import type { ChatMessage, ToolCall } from "@/types/chat";
|
||||
|
||||
export default function ChatPage() {
|
||||
const { t } = useTranslation(["views/chat"]);
|
||||
@@ -62,22 +55,40 @@ export default function ChatPage() {
|
||||
<div className="flex size-full flex-col items-center p-2">
|
||||
<div className="flex min-h-0 w-full flex-1 flex-col gap-2 overflow-y-auto xl:w-[50%]">
|
||||
{messages.map((msg, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={
|
||||
msg.role === "user"
|
||||
? "self-end rounded-lg bg-primary px-3 py-2 text-primary-foreground"
|
||||
: "self-start rounded-lg bg-muted px-3 py-2"
|
||||
}
|
||||
>
|
||||
{msg.role === "assistant" ? (
|
||||
<AssistantMessage
|
||||
content={msg.content}
|
||||
toolCalls={msg.toolCalls}
|
||||
/>
|
||||
) : (
|
||||
msg.content
|
||||
<div key={i} className="flex flex-col gap-2">
|
||||
{msg.role === "assistant" && msg.toolCalls && (
|
||||
<>
|
||||
{msg.toolCalls.map((tc, tcIdx) => (
|
||||
<div key={tcIdx} className="flex flex-col gap-2">
|
||||
<ToolCallBubble
|
||||
name={tc.name}
|
||||
arguments={tc.arguments}
|
||||
side="left"
|
||||
/>
|
||||
{tc.response && (
|
||||
<ToolCallBubble
|
||||
name={tc.name}
|
||||
response={tc.response}
|
||||
side="right"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
className={
|
||||
msg.role === "user"
|
||||
? "self-end rounded-lg bg-primary px-3 py-2 text-primary-foreground"
|
||||
: "self-start rounded-lg bg-muted px-3 py-2"
|
||||
}
|
||||
>
|
||||
{msg.role === "assistant" ? (
|
||||
<AssistantMessage content={msg.content} />
|
||||
) : (
|
||||
msg.content
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{isLoading && (
|
||||
|
||||
11
web/src/types/chat.ts
Normal file
11
web/src/types/chat.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export type ToolCall = {
|
||||
name: string;
|
||||
arguments?: Record<string, unknown>;
|
||||
response?: string;
|
||||
};
|
||||
|
||||
export type ChatMessage = {
|
||||
role: "user" | "assistant";
|
||||
content: string;
|
||||
toolCalls?: ToolCall[];
|
||||
};
|
||||
Reference in New Issue
Block a user