feat: enhance MessageBubble with custom Markdown rendering, update Navbar layout, and integrate Search component in App
This commit is contained in:
@@ -322,7 +322,35 @@ export default function Message(props: { messageIndex: number }) {
|
|||||||
) : chat.role === "tool" ? (
|
) : chat.role === "tool" ? (
|
||||||
<MessageToolResp chat={chat} copyToClipboard={copyToClipboard} />
|
<MessageToolResp chat={chat} copyToClipboard={copyToClipboard} />
|
||||||
) : renderMarkdown ? (
|
) : renderMarkdown ? (
|
||||||
<Markdown>{getMessageText(chat)}</Markdown>
|
<div className="message-content max-w-full md:max-w-[75%]">
|
||||||
|
<Markdown
|
||||||
|
break={true}
|
||||||
|
components={{
|
||||||
|
code: ({ children }) => (
|
||||||
|
<code className="bg-muted px-1 py-0.5 rounded">
|
||||||
|
{children}
|
||||||
|
</code>
|
||||||
|
),
|
||||||
|
pre: ({ children }) => (
|
||||||
|
<pre className="bg-muted p-4 rounded-lg overflow-auto">
|
||||||
|
{children}
|
||||||
|
</pre>
|
||||||
|
),
|
||||||
|
a: ({ href, children }) => (
|
||||||
|
<a
|
||||||
|
href={href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary hover:underline"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getMessageText(chat)}
|
||||||
|
</Markdown>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="message-content max-w-full md:max-w-[75%]">
|
<div className="message-content max-w-full md:max-w-[75%]">
|
||||||
{chat.content &&
|
{chat.content &&
|
||||||
|
|||||||
@@ -22,7 +22,10 @@ export function ModeToggle() {
|
|||||||
const { setTheme } = useTheme();
|
const { setTheme } = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select onValueChange={(value) => setTheme(value as Theme)}>
|
<Select
|
||||||
|
onValueChange={(value) => setTheme(value as Theme)}
|
||||||
|
defaultValue={useTheme().theme}
|
||||||
|
>
|
||||||
<SelectTrigger className="w-full">
|
<SelectTrigger className="w-full">
|
||||||
<SelectValue placeholder="Select theme" />
|
<SelectValue placeholder="Select theme" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|||||||
@@ -32,113 +32,51 @@ import { getTotalCost } from "@/utils/totalCost";
|
|||||||
import { Tr } from "@/translate";
|
import { Tr } from "@/translate";
|
||||||
|
|
||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
import Search from "@/components/Search";
|
|
||||||
import Settings from "@/components/Settings";
|
import Settings from "@/components/Settings";
|
||||||
|
import APIListMenu from "./ListAPI";
|
||||||
|
|
||||||
const Navbar: React.FC = () => {
|
const Navbar: React.FC = () => {
|
||||||
const { chatStore, setChatStore } = useContext(AppChatStoreContext);
|
const { chatStore, setChatStore } = useContext(AppChatStoreContext);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="flex sticky top-0 bg-background h-14 shrink-0 items-center border-b z-50">
|
<header className="flex sticky top-0 bg-background h-14 shrink-0 items-center border-b z-50">
|
||||||
|
<div className="flex flex-col w-full">
|
||||||
<div className="flex flex-1 items-center gap-2">
|
<div className="flex flex-1 items-center gap-2">
|
||||||
<div className="flex items-center gap-2 px-3">
|
<div className="flex items-center gap-2 px-3">
|
||||||
<SidebarTrigger />
|
<SidebarTrigger />
|
||||||
<Separator orientation="vertical" className="mr-2 h-4" />
|
<Separator orientation="vertical" className="mr-2 h-4" />
|
||||||
<h1 className="text-lg font-bold">{chatStore.model}</h1>
|
<h1 className="text-lg font-bold">{chatStore.model}</h1>
|
||||||
<div className="flex justify-between items-center gap-2">
|
|
||||||
<div>
|
|
||||||
<div className="dropdown lg:hidden flex items-center gap-2">
|
|
||||||
<Badge variant="outline">
|
|
||||||
{chatStore.totalTokens.toString()}
|
|
||||||
</Badge>
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger>
|
|
||||||
<EllipsisIcon />
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent>
|
|
||||||
<p>
|
|
||||||
Tokens: {chatStore.totalTokens}/{chatStore.maxTokens}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Cut(s): {chatStore.postBeginIndex}/
|
|
||||||
{chatStore.history.filter(({ hide }) => !hide).length}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Cost: ${chatStore.cost?.toFixed(4)} / $
|
|
||||||
{getTotalCost().toFixed(2)}
|
|
||||||
</p>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
<div className="hidden lg:inline-grid">
|
|
||||||
<Menubar>
|
|
||||||
<MenubarMenu>
|
|
||||||
<MenubarTrigger>
|
|
||||||
<WholeWordIcon className="w-4 h-4 mr-2" />{" "}
|
|
||||||
{chatStore.totalTokens}
|
|
||||||
<CircleDollarSignIcon className="w-4 h-4 mx-2" />
|
|
||||||
{chatStore.cost?.toFixed(4)}
|
|
||||||
</MenubarTrigger>
|
|
||||||
<MenubarContent>
|
|
||||||
<MenubarItem>
|
|
||||||
<RulerIcon className="w-4 h-4 mr-2" />
|
|
||||||
Max Length: {chatStore.maxTokens}
|
|
||||||
</MenubarItem>
|
|
||||||
<MenubarItem>
|
|
||||||
<ReceiptIcon className="w-4 h-4 mr-2" />
|
|
||||||
Price:{" "}
|
|
||||||
{models[chatStore.model]?.price?.prompt *
|
|
||||||
1000 *
|
|
||||||
1000}$ / 1M input tokens
|
|
||||||
</MenubarItem>
|
|
||||||
<MenubarItem>
|
|
||||||
<WalletIcon className="w-4 h-4 mr-2" />
|
|
||||||
Total: {getTotalCost().toFixed(
|
|
||||||
2
|
|
||||||
)}$
|
|
||||||
</MenubarItem>
|
|
||||||
<MenubarItem>
|
|
||||||
<ArrowUpDownIcon className="w-4 h-4 mr-2" />
|
|
||||||
{chatStore.streamMode ? (
|
|
||||||
<>
|
|
||||||
<span>{Tr("STREAM")}</span>·
|
|
||||||
<span style={{ color: "gray" }}>{Tr("FETCH")}</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<span style={{ color: "gray" }}>
|
|
||||||
{Tr("STREAM")}
|
|
||||||
</span>
|
|
||||||
·<span>{Tr("FETCH")}</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</MenubarItem>
|
|
||||||
<MenubarItem>
|
|
||||||
<ScissorsIcon className="w-4 h-4 mr-2" />
|
|
||||||
{
|
|
||||||
chatStore.postBeginIndex
|
|
||||||
} / {chatStore.history.length}
|
|
||||||
</MenubarItem>
|
|
||||||
<MenubarSeparator />
|
|
||||||
<MenubarItem disabled>
|
|
||||||
Switch to Model (TODO):
|
|
||||||
</MenubarItem>
|
|
||||||
<MenubarCheckboxItem checked>gpt-4o</MenubarCheckboxItem>
|
|
||||||
<MenubarCheckboxItem>gpt-o1</MenubarCheckboxItem>
|
|
||||||
<MenubarCheckboxItem>gpt-o1-mini</MenubarCheckboxItem>
|
|
||||||
<MenubarCheckboxItem>gpt-o3</MenubarCheckboxItem>
|
|
||||||
</MenubarContent>
|
|
||||||
</MenubarMenu>
|
|
||||||
</Menubar>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex ml-auto gap-2 px-3">
|
<div className="flex ml-auto gap-2 px-3">
|
||||||
<Settings />
|
<Settings />
|
||||||
<Search />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger className="absolute left-1/2 bottom-0 transform -translate-x-1/2 translate-y-1/2">
|
||||||
|
<div className="rounded-full bg-primary/10 hover:bg-primary/20 p-1 cursor-pointer">
|
||||||
|
<EllipsisIcon className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-screen">
|
||||||
|
<div className="flex justify-between items-center px-4 py-2 border-b">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<ReceiptIcon className="w-4 h-4" />
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
Generated Tokens: {chatStore.totalTokens.toString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<WalletIcon className="w-4 h-4" />
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
Cost: ${getTotalCost().toFixed(2)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<APIListMenu />
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -89,6 +89,8 @@ import {
|
|||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { ModeToggle } from "@/components/ModeToggle";
|
import { ModeToggle } from "@/components/ModeToggle";
|
||||||
|
|
||||||
|
import Search from "@/components/Search";
|
||||||
|
|
||||||
import Navbar from "@/components/navbar";
|
import Navbar from "@/components/navbar";
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
@@ -352,7 +354,10 @@ export function App() {
|
|||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
<SidebarFooter>
|
<SidebarFooter>
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
<ModeToggle />
|
<ModeToggle />
|
||||||
|
<Search />
|
||||||
|
</div>
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<Button variant="destructive">{Tr("DEL")}</Button>
|
<Button variant="destructive">{Tr("DEL")}</Button>
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ export default function ChatBOX() {
|
|||||||
token: data.usage?.completion_tokens_details
|
token: data.usage?.completion_tokens_details
|
||||||
? data.usage.completion_tokens -
|
? data.usage.completion_tokens -
|
||||||
data.usage.completion_tokens_details.reasoning_tokens
|
data.usage.completion_tokens_details.reasoning_tokens
|
||||||
: (data.usage.completion_tokens ?? calculate_token_length(msg.content)),
|
: data.usage.completion_tokens ?? calculate_token_length(msg.content),
|
||||||
example: false,
|
example: false,
|
||||||
audio: null,
|
audio: null,
|
||||||
logprobs: data.choices[0]?.logprobs,
|
logprobs: data.choices[0]?.logprobs,
|
||||||
@@ -414,7 +414,6 @@ export default function ChatBOX() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<APIListMenu />
|
|
||||||
<div className="grow flex flex-col p-2 w-full">
|
<div className="grow flex flex-col p-2 w-full">
|
||||||
<ChatMessageList>
|
<ChatMessageList>
|
||||||
{chatStore.history.length === 0 && (
|
{chatStore.history.length === 0 && (
|
||||||
|
|||||||
Reference in New Issue
Block a user