mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-07-26 13:48:59 +03:00
Chat improvements (#23195)
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
* Support token streaming stats * Propogate streaming token stats to chat calls * Show token stats for each image * Add settings to handle token stats and other options * i18n * Use select * Improve mobile layout and spacing
This commit is contained in:
@@ -6,7 +6,6 @@ import {
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type ChatEvent = { id: string; score?: number };
|
||||
|
||||
@@ -37,10 +36,7 @@ export function ChatEventThumbnailsRow({
|
||||
const renderThumb = (event: ChatEvent, isAnchor = false) => (
|
||||
<div
|
||||
key={event.id}
|
||||
className={cn(
|
||||
"relative aspect-square size-32 shrink-0 overflow-hidden rounded-lg",
|
||||
isAnchor && "ring-2 ring-primary",
|
||||
)}
|
||||
className="relative aspect-square size-32 shrink-0 overflow-hidden rounded-lg"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
@@ -71,9 +67,15 @@ export function ChatEventThumbnailsRow({
|
||||
<TooltipContent>{t("open_in_explore")}</TooltipContent>
|
||||
</Tooltip>
|
||||
{isAnchor && (
|
||||
<span className="pointer-events-none absolute left-1 top-1 rounded bg-primary px-1 text-[10px] text-primary-foreground">
|
||||
{t("anchor")}
|
||||
</span>
|
||||
<>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-0 rounded-lg ring-2 ring-inset ring-primary"
|
||||
/>
|
||||
<span className="pointer-events-none absolute left-1 top-1 rounded bg-primary px-1 text-[10px] text-primary-foreground">
|
||||
{t("anchor")}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ChatAttachmentChip } from "@/components/chat/ChatAttachmentChip";
|
||||
import { parseAttachedEvent } from "@/utils/chatUtil";
|
||||
import type { ChatStats, ShowStatsMode } from "@/types/chat";
|
||||
|
||||
type MessageBubbleProps = {
|
||||
role: "user" | "assistant";
|
||||
@@ -24,14 +25,29 @@ type MessageBubbleProps = {
|
||||
messageIndex?: number;
|
||||
onEditSubmit?: (messageIndex: number, newContent: string) => void;
|
||||
isComplete?: boolean;
|
||||
stats?: ChatStats;
|
||||
showStats?: ShowStatsMode;
|
||||
};
|
||||
|
||||
function formatTokens(n: number | undefined): string | null {
|
||||
if (n === undefined) return null;
|
||||
if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
|
||||
return String(n);
|
||||
}
|
||||
|
||||
function formatRate(rate: number | undefined): string | null {
|
||||
if (rate === undefined || rate <= 0) return null;
|
||||
return rate >= 10 ? rate.toFixed(0) : rate.toFixed(1);
|
||||
}
|
||||
|
||||
export function MessageBubble({
|
||||
role,
|
||||
content,
|
||||
messageIndex = 0,
|
||||
onEditSubmit,
|
||||
isComplete = true,
|
||||
stats,
|
||||
showStats = "while_generating",
|
||||
}: MessageBubbleProps) {
|
||||
const { t } = useTranslation(["views/chat", "common"]);
|
||||
const isUser = role === "user";
|
||||
@@ -214,7 +230,7 @@ export function MessageBubble({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<div className="flex items-center gap-1.5">
|
||||
{isUser && onEditSubmit != null && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
@@ -256,6 +272,27 @@ export function MessageBubble({
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{!isUser &&
|
||||
stats &&
|
||||
(showStats === "always" || !isComplete) &&
|
||||
(() => {
|
||||
const ctx = formatTokens(stats.promptTokens);
|
||||
const rate = formatRate(stats.tokensPerSecond);
|
||||
if (ctx === null && rate === null) return null;
|
||||
return (
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
{ctx !== null && (
|
||||
<span>{t("stats.context", { tokens: ctx })}</span>
|
||||
)}
|
||||
{ctx !== null && rate !== null && (
|
||||
<span aria-hidden="true">·</span>
|
||||
)}
|
||||
{rate !== null && (
|
||||
<span>{t("stats.tokens_per_second", { rate })}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useState } from "react";
|
||||
import { isDesktop } from "react-device-detect";
|
||||
import { cn } from "@/lib/utils";
|
||||
import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog";
|
||||
import { FaCog } from "react-icons/fa";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { DropdownMenuSeparator } from "@/components/ui/dropdown-menu";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { ShowStatsMode } from "@/types/chat";
|
||||
|
||||
type ChatSettingsProps = {
|
||||
showStats: ShowStatsMode;
|
||||
setShowStats: (mode: ShowStatsMode) => void;
|
||||
autoScroll: boolean;
|
||||
setAutoScroll: (enabled: boolean) => void;
|
||||
};
|
||||
|
||||
export default function ChatSettings({
|
||||
showStats,
|
||||
setShowStats,
|
||||
autoScroll,
|
||||
setAutoScroll,
|
||||
}: ChatSettingsProps) {
|
||||
const { t } = useTranslation(["views/chat"]);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const trigger = (
|
||||
<Button
|
||||
className="flex items-center md:gap-2"
|
||||
aria-label={t("settings.title")}
|
||||
size="sm"
|
||||
>
|
||||
<FaCog className="text-secondary-foreground" />
|
||||
<span className="hidden md:inline">{t("settings.title")}</span>
|
||||
</Button>
|
||||
);
|
||||
|
||||
const content = (
|
||||
<div className="my-3 space-y-5 py-3 md:mt-0 md:py-0">
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-0.5">
|
||||
<div className="text-md">{t("settings.show_stats.title")}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{t("settings.show_stats.desc")}
|
||||
</div>
|
||||
</div>
|
||||
<Select
|
||||
value={showStats}
|
||||
onValueChange={(v) => setShowStats(v as ShowStatsMode)}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
{showStats === "always"
|
||||
? t("settings.show_stats.always")
|
||||
: t("settings.show_stats.while_generating")}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem className="cursor-pointer" value="while_generating">
|
||||
{t("settings.show_stats.while_generating")}
|
||||
</SelectItem>
|
||||
<SelectItem className="cursor-pointer" value="always">
|
||||
{t("settings.show_stats.always")}
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<DropdownMenuSeparator />
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="auto-scroll" className="text-md cursor-pointer">
|
||||
{t("settings.auto_scroll.title")}
|
||||
</Label>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{t("settings.auto_scroll.desc")}
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
id="auto-scroll"
|
||||
checked={autoScroll}
|
||||
onCheckedChange={setAutoScroll}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<PlatformAwareDialog
|
||||
trigger={trigger}
|
||||
content={content}
|
||||
contentClassName={cn(
|
||||
"scrollbar-container h-auto overflow-y-auto",
|
||||
isDesktop ? "max-h-[80dvh] w-72" : "px-4",
|
||||
)}
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -54,7 +54,9 @@ export function ChatStartingState({ onSendMessage }: ChatStartingStateProps) {
|
||||
<div className="flex size-full flex-col items-center justify-center gap-6 p-8">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<h1 className="text-4xl font-bold text-foreground">{t("title")}</h1>
|
||||
<p className="text-muted-foreground">{t("subtitle")}</p>
|
||||
<p className="text-center text-muted-foreground md:text-left">
|
||||
{t("subtitle")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full max-w-2xl flex-col items-center gap-4">
|
||||
|
||||
Reference in New Issue
Block a user