chore: remove tokenlens and token usage tracking (#1357)
This commit is contained in:
parent
5f9e231788
commit
7942e97095
17 changed files with 60 additions and 773 deletions
|
|
@ -1,408 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import type { LanguageModelUsage } from "ai";
|
||||
import { type ComponentProps, createContext, useContext } from "react";
|
||||
import { getUsage } from "tokenlens";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from "@/components/ui/hover-card";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const PERCENT_MAX = 100;
|
||||
const ICON_RADIUS = 10;
|
||||
const ICON_VIEWBOX = 24;
|
||||
const ICON_CENTER = 12;
|
||||
const ICON_STROKE_WIDTH = 2;
|
||||
|
||||
type ModelId = string;
|
||||
|
||||
type ContextSchema = {
|
||||
usedTokens: number;
|
||||
maxTokens: number;
|
||||
usage?: LanguageModelUsage;
|
||||
modelId?: ModelId;
|
||||
};
|
||||
|
||||
const ContextContext = createContext<ContextSchema | null>(null);
|
||||
|
||||
const useContextValue = () => {
|
||||
const context = useContext(ContextContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("Context components must be used within Context");
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
export type ContextProps = ComponentProps<typeof HoverCard> & ContextSchema;
|
||||
|
||||
export const Context = ({
|
||||
usedTokens,
|
||||
maxTokens,
|
||||
usage,
|
||||
modelId,
|
||||
...props
|
||||
}: ContextProps) => (
|
||||
<ContextContext.Provider
|
||||
value={{
|
||||
usedTokens,
|
||||
maxTokens,
|
||||
usage,
|
||||
modelId,
|
||||
}}
|
||||
>
|
||||
<HoverCard closeDelay={0} openDelay={0} {...props} />
|
||||
</ContextContext.Provider>
|
||||
);
|
||||
|
||||
const ContextIcon = () => {
|
||||
const { usedTokens, maxTokens } = useContextValue();
|
||||
const circumference = 2 * Math.PI * ICON_RADIUS;
|
||||
const usedPercent = usedTokens / maxTokens;
|
||||
const dashOffset = circumference * (1 - usedPercent);
|
||||
|
||||
return (
|
||||
<svg
|
||||
aria-label="Model context usage"
|
||||
height="20"
|
||||
role="img"
|
||||
style={{ color: "currentcolor" }}
|
||||
viewBox={`0 0 ${ICON_VIEWBOX} ${ICON_VIEWBOX}`}
|
||||
width="20"
|
||||
>
|
||||
<circle
|
||||
cx={ICON_CENTER}
|
||||
cy={ICON_CENTER}
|
||||
fill="none"
|
||||
opacity="0.25"
|
||||
r={ICON_RADIUS}
|
||||
stroke="currentColor"
|
||||
strokeWidth={ICON_STROKE_WIDTH}
|
||||
/>
|
||||
<circle
|
||||
cx={ICON_CENTER}
|
||||
cy={ICON_CENTER}
|
||||
fill="none"
|
||||
opacity="0.7"
|
||||
r={ICON_RADIUS}
|
||||
stroke="currentColor"
|
||||
strokeDasharray={`${circumference} ${circumference}`}
|
||||
strokeDashoffset={dashOffset}
|
||||
strokeLinecap="round"
|
||||
strokeWidth={ICON_STROKE_WIDTH}
|
||||
style={{ transformOrigin: "center", transform: "rotate(-90deg)" }}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export type ContextTriggerProps = ComponentProps<typeof Button>;
|
||||
|
||||
export const ContextTrigger = ({ children, ...props }: ContextTriggerProps) => {
|
||||
const { usedTokens, maxTokens } = useContextValue();
|
||||
const usedPercent = usedTokens / maxTokens;
|
||||
const renderedPercent = new Intl.NumberFormat("en-US", {
|
||||
style: "percent",
|
||||
maximumFractionDigits: 1,
|
||||
}).format(usedPercent);
|
||||
|
||||
return (
|
||||
<HoverCardTrigger asChild>
|
||||
{children ?? (
|
||||
<Button type="button" variant="ghost" {...props}>
|
||||
<span className="font-medium text-muted-foreground">
|
||||
{renderedPercent}
|
||||
</span>
|
||||
<ContextIcon />
|
||||
</Button>
|
||||
)}
|
||||
</HoverCardTrigger>
|
||||
);
|
||||
};
|
||||
|
||||
export type ContextContentProps = ComponentProps<typeof HoverCardContent>;
|
||||
|
||||
export const ContextContent = ({
|
||||
className,
|
||||
...props
|
||||
}: ContextContentProps) => (
|
||||
<HoverCardContent
|
||||
className={cn("min-w-60 divide-y overflow-hidden p-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export type ContextContentHeaderProps = ComponentProps<"div">;
|
||||
|
||||
export const ContextContentHeader = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: ContextContentHeaderProps) => {
|
||||
const { usedTokens, maxTokens } = useContextValue();
|
||||
const usedPercent = usedTokens / maxTokens;
|
||||
const displayPct = new Intl.NumberFormat("en-US", {
|
||||
style: "percent",
|
||||
maximumFractionDigits: 1,
|
||||
}).format(usedPercent);
|
||||
const used = new Intl.NumberFormat("en-US", {
|
||||
notation: "compact",
|
||||
}).format(usedTokens);
|
||||
const total = new Intl.NumberFormat("en-US", {
|
||||
notation: "compact",
|
||||
}).format(maxTokens);
|
||||
|
||||
return (
|
||||
<div className={cn("w-full space-y-2 p-3", className)} {...props}>
|
||||
{children ?? (
|
||||
<>
|
||||
<div className="flex items-center justify-between gap-3 text-xs">
|
||||
<p>{displayPct}</p>
|
||||
<p className="font-mono text-muted-foreground">
|
||||
{used} / {total}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Progress className="bg-muted" value={usedPercent * PERCENT_MAX} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export type ContextContentBodyProps = ComponentProps<"div">;
|
||||
|
||||
export const ContextContentBody = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: ContextContentBodyProps) => (
|
||||
<div className={cn("w-full p-3", className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export type ContextContentFooterProps = ComponentProps<"div">;
|
||||
|
||||
export const ContextContentFooter = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: ContextContentFooterProps) => {
|
||||
const { modelId, usage } = useContextValue();
|
||||
const costUSD = modelId
|
||||
? getUsage({
|
||||
modelId,
|
||||
usage: {
|
||||
input: usage?.inputTokens ?? 0,
|
||||
output: usage?.outputTokens ?? 0,
|
||||
},
|
||||
}).costUSD?.totalUSD
|
||||
: undefined;
|
||||
const totalCost = new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
}).format(costUSD ?? 0);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex w-full items-center justify-between gap-3 bg-secondary p-3 text-xs",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children ?? (
|
||||
<>
|
||||
<span className="text-muted-foreground">Total cost</span>
|
||||
<span>{totalCost}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export type ContextInputUsageProps = ComponentProps<"div">;
|
||||
|
||||
export const ContextInputUsage = ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: ContextInputUsageProps) => {
|
||||
const { usage, modelId } = useContextValue();
|
||||
const inputTokens = usage?.inputTokens ?? 0;
|
||||
|
||||
if (children) {
|
||||
return children;
|
||||
}
|
||||
|
||||
if (!inputTokens) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const inputCost = modelId
|
||||
? getUsage({
|
||||
modelId,
|
||||
usage: { input: inputTokens, output: 0 },
|
||||
}).costUSD?.totalUSD
|
||||
: undefined;
|
||||
const inputCostText = new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
}).format(inputCost ?? 0);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("flex items-center justify-between text-xs", className)}
|
||||
{...props}
|
||||
>
|
||||
<span className="text-muted-foreground">Input</span>
|
||||
<TokensWithCost costText={inputCostText} tokens={inputTokens} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export type ContextOutputUsageProps = ComponentProps<"div">;
|
||||
|
||||
export const ContextOutputUsage = ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: ContextOutputUsageProps) => {
|
||||
const { usage, modelId } = useContextValue();
|
||||
const outputTokens = usage?.outputTokens ?? 0;
|
||||
|
||||
if (children) {
|
||||
return children;
|
||||
}
|
||||
|
||||
if (!outputTokens) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const outputCost = modelId
|
||||
? getUsage({
|
||||
modelId,
|
||||
usage: { input: 0, output: outputTokens },
|
||||
}).costUSD?.totalUSD
|
||||
: undefined;
|
||||
const outputCostText = new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
}).format(outputCost ?? 0);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("flex items-center justify-between text-xs", className)}
|
||||
{...props}
|
||||
>
|
||||
<span className="text-muted-foreground">Output</span>
|
||||
<TokensWithCost costText={outputCostText} tokens={outputTokens} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export type ContextReasoningUsageProps = ComponentProps<"div">;
|
||||
|
||||
export const ContextReasoningUsage = ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: ContextReasoningUsageProps) => {
|
||||
const { usage, modelId } = useContextValue();
|
||||
const reasoningTokens = usage?.reasoningTokens ?? 0;
|
||||
|
||||
if (children) {
|
||||
return children;
|
||||
}
|
||||
|
||||
if (!reasoningTokens) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const reasoningCost = modelId
|
||||
? getUsage({
|
||||
modelId,
|
||||
usage: { reasoningTokens },
|
||||
}).costUSD?.totalUSD
|
||||
: undefined;
|
||||
const reasoningCostText = new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
}).format(reasoningCost ?? 0);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("flex items-center justify-between text-xs", className)}
|
||||
{...props}
|
||||
>
|
||||
<span className="text-muted-foreground">Reasoning</span>
|
||||
<TokensWithCost costText={reasoningCostText} tokens={reasoningTokens} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export type ContextCacheUsageProps = ComponentProps<"div">;
|
||||
|
||||
export const ContextCacheUsage = ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: ContextCacheUsageProps) => {
|
||||
const { usage, modelId } = useContextValue();
|
||||
const cacheTokens = usage?.cachedInputTokens ?? 0;
|
||||
|
||||
if (children) {
|
||||
return children;
|
||||
}
|
||||
|
||||
if (!cacheTokens) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cacheCost = modelId
|
||||
? getUsage({
|
||||
modelId,
|
||||
usage: { cacheReads: cacheTokens, input: 0, output: 0 },
|
||||
}).costUSD?.totalUSD
|
||||
: undefined;
|
||||
const cacheCostText = new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
}).format(cacheCost ?? 0);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("flex items-center justify-between text-xs", className)}
|
||||
{...props}
|
||||
>
|
||||
<span className="text-muted-foreground">Cache</span>
|
||||
<TokensWithCost costText={cacheCostText} tokens={cacheTokens} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const TokensWithCost = ({
|
||||
tokens,
|
||||
costText,
|
||||
}: {
|
||||
tokens?: number;
|
||||
costText?: string;
|
||||
}) => (
|
||||
<span>
|
||||
{tokens === undefined
|
||||
? "—"
|
||||
: new Intl.NumberFormat("en-US", {
|
||||
notation: "compact",
|
||||
}).format(tokens)}
|
||||
{costText ? (
|
||||
<span className="ml-2 text-muted-foreground">• {costText}</span>
|
||||
) : null}
|
||||
</span>
|
||||
);
|
||||
|
|
@ -49,8 +49,9 @@ export function AppSidebar({ user }: { user: User | undefined }) {
|
|||
loading: "Deleting all chats...",
|
||||
success: () => {
|
||||
mutate(unstable_serialize(getChatHistoryPaginationKey));
|
||||
router.push("/");
|
||||
setShowDeleteAllDialog(false);
|
||||
router.replace("/");
|
||||
router.refresh();
|
||||
return "All chats deleted successfully";
|
||||
},
|
||||
error: "Failed to delete all chats",
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ import { useChatVisibility } from "@/hooks/use-chat-visibility";
|
|||
import type { Vote } from "@/lib/db/schema";
|
||||
import { ChatSDKError } from "@/lib/errors";
|
||||
import type { Attachment, ChatMessage } from "@/lib/types";
|
||||
import type { AppUsage } from "@/lib/usage";
|
||||
import { fetcher, fetchWithErrorHandlers, generateUUID } from "@/lib/utils";
|
||||
import { Artifact } from "./artifact";
|
||||
import { useDataStream } from "./data-stream-provider";
|
||||
|
|
@ -40,7 +39,6 @@ export function Chat({
|
|||
initialVisibilityType,
|
||||
isReadonly,
|
||||
autoResume,
|
||||
initialLastContext,
|
||||
}: {
|
||||
id: string;
|
||||
initialMessages: ChatMessage[];
|
||||
|
|
@ -48,7 +46,6 @@ export function Chat({
|
|||
initialVisibilityType: VisibilityType;
|
||||
isReadonly: boolean;
|
||||
autoResume: boolean;
|
||||
initialLastContext?: AppUsage;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
|
||||
|
|
@ -72,7 +69,6 @@ export function Chat({
|
|||
const { setDataStream } = useDataStream();
|
||||
|
||||
const [input, setInput] = useState<string>("");
|
||||
const [usage, setUsage] = useState<AppUsage | undefined>(initialLastContext);
|
||||
const [showCreditCardAlert, setShowCreditCardAlert] = useState(false);
|
||||
const [currentModelId, setCurrentModelId] = useState(initialChatModel);
|
||||
const currentModelIdRef = useRef(currentModelId);
|
||||
|
|
@ -111,9 +107,6 @@ export function Chat({
|
|||
}),
|
||||
onData: (dataPart) => {
|
||||
setDataStream((ds) => (ds ? [...ds, dataPart] : []));
|
||||
if (dataPart.type === "data-usage") {
|
||||
setUsage(dataPart.data);
|
||||
}
|
||||
},
|
||||
onFinish: () => {
|
||||
mutate(unstable_serialize(getChatHistoryPaginationKey));
|
||||
|
|
@ -204,7 +197,6 @@ export function Chat({
|
|||
setMessages={setMessages}
|
||||
status={status}
|
||||
stop={stop}
|
||||
usage={usage}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
import { unstable_serialize } from "swr/infinite";
|
||||
import { initialArtifactData, useArtifact } from "@/hooks/use-artifact";
|
||||
import { artifactDefinitions } from "./artifact";
|
||||
import { useDataStream } from "./data-stream-provider";
|
||||
import { getChatHistoryPaginationKey } from "./sidebar-history";
|
||||
|
||||
export function DataStreamHandler() {
|
||||
const { dataStream, setDataStream } = useDataStream();
|
||||
const { mutate } = useSWRConfig();
|
||||
|
||||
const { artifact, setArtifact, setMetadata } = useArtifact();
|
||||
|
||||
|
|
@ -19,6 +23,11 @@ export function DataStreamHandler() {
|
|||
setDataStream([]);
|
||||
|
||||
for (const delta of newDeltas) {
|
||||
// Handle chat title updates
|
||||
if (delta.type === "data-chat-title") {
|
||||
mutate(unstable_serialize(getChatHistoryPaginationKey));
|
||||
continue;
|
||||
}
|
||||
const artifactDefinition = artifactDefinitions.find(
|
||||
(currentArtifactDefinition) =>
|
||||
currentArtifactDefinition.kind === artifact.kind
|
||||
|
|
@ -77,7 +86,7 @@ export function DataStreamHandler() {
|
|||
}
|
||||
});
|
||||
}
|
||||
}, [dataStream, setArtifact, setMetadata, artifact, setDataStream]);
|
||||
}, [dataStream, setArtifact, setMetadata, artifact, setDataStream, mutate]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,190 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import type { ComponentProps } from "react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import type { AppUsage } from "@/lib/usage";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type ContextProps = ComponentProps<"button"> & {
|
||||
/** Optional full usage payload to enable breakdown view */
|
||||
usage?: AppUsage;
|
||||
};
|
||||
|
||||
const _THOUSAND = 1000;
|
||||
const _MILLION = 1_000_000;
|
||||
const _BILLION = 1_000_000_000;
|
||||
const PERCENT_MAX = 100;
|
||||
|
||||
// Lucide CircleIcon geometry
|
||||
const ICON_VIEWBOX = 24;
|
||||
const ICON_CENTER = 12;
|
||||
const ICON_RADIUS = 10;
|
||||
const ICON_STROKE_WIDTH = 2;
|
||||
|
||||
type ContextIconProps = {
|
||||
percent: number; // 0 - 100
|
||||
};
|
||||
|
||||
export const ContextIcon = ({ percent }: ContextIconProps) => {
|
||||
const radius = ICON_RADIUS;
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
const dashOffset = circumference * (1 - percent / PERCENT_MAX);
|
||||
|
||||
return (
|
||||
<svg
|
||||
aria-label={`${percent.toFixed(2)}% of model context used`}
|
||||
height="28"
|
||||
role="img"
|
||||
style={{ color: "currentcolor" }}
|
||||
viewBox={`0 0 ${ICON_VIEWBOX} ${ICON_VIEWBOX}`}
|
||||
width="28"
|
||||
>
|
||||
<circle
|
||||
cx={ICON_CENTER}
|
||||
cy={ICON_CENTER}
|
||||
fill="none"
|
||||
opacity="0.25"
|
||||
r={radius}
|
||||
stroke="currentColor"
|
||||
strokeWidth={ICON_STROKE_WIDTH}
|
||||
/>
|
||||
<circle
|
||||
cx={ICON_CENTER}
|
||||
cy={ICON_CENTER}
|
||||
fill="none"
|
||||
opacity="0.7"
|
||||
r={radius}
|
||||
stroke="currentColor"
|
||||
strokeDasharray={`${circumference} ${circumference}`}
|
||||
strokeDashoffset={dashOffset}
|
||||
strokeLinecap="round"
|
||||
strokeWidth={ICON_STROKE_WIDTH}
|
||||
transform={`rotate(-90 ${ICON_CENTER} ${ICON_CENTER})`}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
function InfoRow({
|
||||
label,
|
||||
tokens,
|
||||
costText,
|
||||
}: {
|
||||
label: string;
|
||||
tokens?: number;
|
||||
costText?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-muted-foreground">{label}</span>
|
||||
<div className="flex items-center gap-2 font-mono">
|
||||
<span className="min-w-[4ch] text-right">
|
||||
{tokens === undefined ? "—" : tokens.toLocaleString()}
|
||||
</span>
|
||||
{costText !== undefined &&
|
||||
costText !== null &&
|
||||
!Number.isNaN(Number.parseFloat(costText)) && (
|
||||
<span className="text-muted-foreground">
|
||||
${Number.parseFloat(costText).toFixed(6)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const Context = ({ className, usage, ...props }: ContextProps) => {
|
||||
const used = usage?.totalTokens ?? 0;
|
||||
const max =
|
||||
usage?.context?.totalMax ??
|
||||
usage?.context?.combinedMax ??
|
||||
usage?.context?.inputMax;
|
||||
const hasMax = typeof max === "number" && Number.isFinite(max) && max > 0;
|
||||
const usedPercent = hasMax ? Math.min(100, (used / max) * 100) : 0;
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
className={cn(
|
||||
"inline-flex select-none items-center gap-1 rounded-md text-sm",
|
||||
"cursor-pointer bg-background text-foreground",
|
||||
"outline-none ring-offset-background focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
className
|
||||
)}
|
||||
type="button"
|
||||
{...props}
|
||||
>
|
||||
<span className="hidden font-medium text-muted-foreground">
|
||||
{usedPercent.toFixed(1)}%
|
||||
</span>
|
||||
<ContextIcon percent={usedPercent} />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-fit p-3" side="top">
|
||||
<div className="min-w-[240px] space-y-2">
|
||||
<div className="flex items-start justify-between text-sm">
|
||||
<span>{usedPercent.toFixed(1)}%</span>
|
||||
<span className="text-muted-foreground">
|
||||
{hasMax ? `${used} / ${max} tokens` : `${used} tokens`}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Progress className="h-2 bg-muted" value={usedPercent} />
|
||||
</div>
|
||||
<div className="mt-1 space-y-1">
|
||||
{usage?.cachedInputTokens && usage.cachedInputTokens > 0 && (
|
||||
<InfoRow
|
||||
costText={usage?.costUSD?.cacheReadUSD?.toString()}
|
||||
label="Cache Hits"
|
||||
tokens={usage?.cachedInputTokens}
|
||||
/>
|
||||
)}
|
||||
<InfoRow
|
||||
costText={usage?.costUSD?.inputUSD?.toString()}
|
||||
label="Input"
|
||||
tokens={usage?.inputTokens}
|
||||
/>
|
||||
<InfoRow
|
||||
costText={usage?.costUSD?.outputUSD?.toString()}
|
||||
label="Output"
|
||||
tokens={usage?.outputTokens}
|
||||
/>
|
||||
<InfoRow
|
||||
costText={usage?.costUSD?.reasoningUSD?.toString()}
|
||||
label="Reasoning"
|
||||
tokens={
|
||||
usage?.reasoningTokens && usage.reasoningTokens > 0
|
||||
? usage.reasoningTokens
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
{usage?.costUSD?.totalUSD !== undefined && (
|
||||
<>
|
||||
<Separator className="mt-1" />
|
||||
<div className="flex items-center justify-between pt-1 text-xs">
|
||||
<span className="text-muted-foreground">Total cost</span>
|
||||
<div className="flex items-center gap-2 font-mono">
|
||||
<span className="min-w-[4ch] text-right" />
|
||||
<span>
|
||||
{Number.isNaN(
|
||||
Number.parseFloat(usage.costUSD.totalUSD.toString())
|
||||
)
|
||||
? "—"
|
||||
: `$${Number.parseFloat(usage.costUSD.totalUSD.toString()).toFixed(6)}`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
|
|
@ -34,9 +34,7 @@ import {
|
|||
modelsByProvider,
|
||||
} from "@/lib/ai/models";
|
||||
import type { Attachment, ChatMessage } from "@/lib/types";
|
||||
import type { AppUsage } from "@/lib/usage";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Context } from "./elements/context";
|
||||
import {
|
||||
PromptInput,
|
||||
PromptInputSubmit,
|
||||
|
|
@ -71,7 +69,6 @@ function PureMultimodalInput({
|
|||
selectedVisibilityType,
|
||||
selectedModelId,
|
||||
onModelChange,
|
||||
usage,
|
||||
}: {
|
||||
chatId: string;
|
||||
input: string;
|
||||
|
|
@ -87,7 +84,6 @@ function PureMultimodalInput({
|
|||
selectedVisibilityType: VisibilityType;
|
||||
selectedModelId: string;
|
||||
onModelChange?: (modelId: string) => void;
|
||||
usage?: AppUsage;
|
||||
}) {
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const { width } = useWindowSize();
|
||||
|
|
@ -204,13 +200,6 @@ function PureMultimodalInput({
|
|||
}
|
||||
}, []);
|
||||
|
||||
const contextProps = useMemo(
|
||||
() => ({
|
||||
usage,
|
||||
}),
|
||||
[usage]
|
||||
);
|
||||
|
||||
const handleFileChange = useCallback(
|
||||
async (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(event.target.files || []);
|
||||
|
|
@ -374,8 +363,7 @@ function PureMultimodalInput({
|
|||
ref={textareaRef}
|
||||
rows={1}
|
||||
value={input}
|
||||
/>{" "}
|
||||
<Context {...contextProps} />
|
||||
/>
|
||||
</div>
|
||||
<PromptInputToolbar className="border-top-0! border-t-0! p-0 shadow-none dark:border-0 dark:border-transparent!">
|
||||
<PromptInputTools className="gap-0 sm:gap-0.5">
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { isToday, isYesterday, subMonths, subWeeks } from "date-fns";
|
||||
import { motion } from "framer-motion";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import type { User } from "next-auth";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
|
@ -99,7 +99,8 @@ export function getChatHistoryPaginationKey(
|
|||
|
||||
export function SidebarHistory({ user }: { user: User | undefined }) {
|
||||
const { setOpenMobile } = useSidebar();
|
||||
const { id } = useParams();
|
||||
const pathname = usePathname();
|
||||
const id = pathname?.startsWith("/chat/") ? pathname.split("/")[2] : null;
|
||||
|
||||
const {
|
||||
data: paginatedChatHistories,
|
||||
|
|
@ -124,7 +125,12 @@ export function SidebarHistory({ user }: { user: User | undefined }) {
|
|||
: false;
|
||||
|
||||
const handleDelete = () => {
|
||||
const deletePromise = fetch(`/api/chat?id=${deleteId}`, {
|
||||
const chatToDelete = deleteId;
|
||||
const isCurrentChat = pathname === `/chat/${chatToDelete}`;
|
||||
|
||||
setShowDeleteDialog(false);
|
||||
|
||||
const deletePromise = fetch(`/api/chat?id=${chatToDelete}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
|
|
@ -135,21 +141,22 @@ export function SidebarHistory({ user }: { user: User | undefined }) {
|
|||
if (chatHistories) {
|
||||
return chatHistories.map((chatHistory) => ({
|
||||
...chatHistory,
|
||||
chats: chatHistory.chats.filter((chat) => chat.id !== deleteId),
|
||||
chats: chatHistory.chats.filter(
|
||||
(chat) => chat.id !== chatToDelete
|
||||
),
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
if (isCurrentChat) {
|
||||
router.replace("/");
|
||||
router.refresh();
|
||||
}
|
||||
|
||||
return "Chat deleted successfully";
|
||||
},
|
||||
error: "Failed to delete chat",
|
||||
});
|
||||
|
||||
setShowDeleteDialog(false);
|
||||
|
||||
if (deleteId === id) {
|
||||
router.push("/");
|
||||
}
|
||||
};
|
||||
|
||||
if (!user) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue