mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-14 04:17:34 +03:00
Show tool calls separately from message
This commit is contained in:
parent
eb9f16b4fa
commit
3e97f9e985
@ -4,5 +4,7 @@
|
|||||||
"processing": "Processing...",
|
"processing": "Processing...",
|
||||||
"toolsUsed": "Used: {{tools}}",
|
"toolsUsed": "Used: {{tools}}",
|
||||||
"showTools": "Show tools ({{count}})",
|
"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 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 = {
|
type AssistantMessageProps = {
|
||||||
content: string;
|
content: string;
|
||||||
toolCalls?: ToolCall[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function AssistantMessage({
|
export function AssistantMessage({ content }: AssistantMessageProps) {
|
||||||
content,
|
return <ReactMarkdown>{content}</ReactMarkdown>;
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
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 { useTranslation } from "react-i18next";
|
||||||
import { useState, useCallback } from "react";
|
import { useState, useCallback } from "react";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import {
|
import { AssistantMessage } from "@/components/chat/AssistantMessage";
|
||||||
AssistantMessage,
|
import { ToolCallBubble } from "@/components/chat/ToolCallBubble";
|
||||||
type ToolCall,
|
import type { ChatMessage, ToolCall } from "@/types/chat";
|
||||||
} from "@/components/chat/AssistantMessage";
|
|
||||||
|
|
||||||
type ChatMessage = {
|
|
||||||
role: "user" | "assistant";
|
|
||||||
content: string;
|
|
||||||
toolCalls?: ToolCall[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function ChatPage() {
|
export default function ChatPage() {
|
||||||
const { t } = useTranslation(["views/chat"]);
|
const { t } = useTranslation(["views/chat"]);
|
||||||
@ -62,8 +55,28 @@ export default function ChatPage() {
|
|||||||
<div className="flex size-full flex-col items-center p-2">
|
<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%]">
|
<div className="flex min-h-0 w-full flex-1 flex-col gap-2 overflow-y-auto xl:w-[50%]">
|
||||||
{messages.map((msg, i) => (
|
{messages.map((msg, i) => (
|
||||||
|
<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
|
<div
|
||||||
key={i}
|
|
||||||
className={
|
className={
|
||||||
msg.role === "user"
|
msg.role === "user"
|
||||||
? "self-end rounded-lg bg-primary px-3 py-2 text-primary-foreground"
|
? "self-end rounded-lg bg-primary px-3 py-2 text-primary-foreground"
|
||||||
@ -71,14 +84,12 @@ export default function ChatPage() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{msg.role === "assistant" ? (
|
{msg.role === "assistant" ? (
|
||||||
<AssistantMessage
|
<AssistantMessage content={msg.content} />
|
||||||
content={msg.content}
|
|
||||||
toolCalls={msg.toolCalls}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
msg.content
|
msg.content
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="self-start rounded-lg bg-muted px-3 py-2 text-muted-foreground">
|
<div className="self-start rounded-lg bg-muted px-3 py-2 text-muted-foreground">
|
||||||
|
|||||||
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[];
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user