Skip to content

Commit 2e9e81e

Browse files
authored
Merge pull request #89 from jakobhoeg/feature/action-buttons
feat: add action buttons (regenerate, copy)
2 parents 4046ec5 + acebfd3 commit 2e9e81e

File tree

5 files changed

+140
-32
lines changed

5 files changed

+140
-32
lines changed

src/app/hooks/useChatStore.ts

Lines changed: 9 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,23 @@ interface State {
1313
currentChatId: string | null;
1414
selectedModel: string | null;
1515
userName: string | "Anonymous";
16-
isDownloading: boolean; // New: Track download state
17-
downloadProgress: number; // New: Track download progress
18-
downloadingModel: string | null; // New: Track which model is being downloaded
16+
isDownloading: boolean;
17+
downloadProgress: number;
18+
downloadingModel: string | null;
1919
}
2020

2121
interface Actions {
2222
setBase64Images: (base64Images: string[] | null) => void;
23-
setMessages: (chatId: string, fn: (messages: Message[]) => Message[]) => void;
2423
setCurrentChatId: (chatId: string) => void;
2524
setSelectedModel: (selectedModel: string) => void;
2625
getChatById: (chatId: string) => ChatSession | undefined;
2726
getMessagesById: (chatId: string) => Message[];
2827
saveMessages: (chatId: string, messages: Message[]) => void;
2928
handleDelete: (chatId: string, messageId?: string) => void;
3029
setUserName: (userName: string) => void;
31-
startDownload: (modelName: string) => void; // New: Start download
32-
stopDownload: () => void; // New: Stop download
33-
setDownloadProgress: (progress: number) => void; // New: Update progress
30+
startDownload: (modelName: string) => void;
31+
stopDownload: () => void;
32+
setDownloadProgress: (progress: number) => void;
3433
}
3534

3635
const useChatStore = create<State & Actions>()(
@@ -41,29 +40,13 @@ const useChatStore = create<State & Actions>()(
4140
currentChatId: null,
4241
selectedModel: null,
4342
userName: "Anonymous",
44-
isDownloading: false, // Default download state
45-
downloadProgress: 0, // Default progress
46-
downloadingModel: null, // Default downloading model
43+
isDownloading: false,
44+
downloadProgress: 0,
45+
downloadingModel: null,
4746

48-
// Existing actions
4947
setBase64Images: (base64Images) => set({ base64Images }),
5048
setUserName: (userName) => set({ userName }),
51-
setMessages: (chatId, fn) =>
52-
set((state) => {
53-
const existingChat = state.chats[chatId];
54-
const updatedMessages = fn(existingChat?.messages || []);
5549

56-
return {
57-
chats: {
58-
...state.chats,
59-
[chatId]: {
60-
...existingChat,
61-
messages: updatedMessages,
62-
createdAt: existingChat?.createdAt || new Date().toISOString(),
63-
},
64-
},
65-
};
66-
}),
6750
setCurrentChatId: (chatId) => set({ currentChatId: chatId }),
6851
setSelectedModel: (selectedModel) => set({ selectedModel }),
6952
getChatById: (chatId) => {
@@ -118,7 +101,6 @@ const useChatStore = create<State & Actions>()(
118101
});
119102
},
120103

121-
// New actions for download state
122104
startDownload: (modelName) =>
123105
set({ isDownloading: true, downloadingModel: modelName, downloadProgress: 0 }),
124106
stopDownload: () =>
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import React, { forwardRef } from "react";
2+
import { Button } from "./ui/button";
3+
import {
4+
Tooltip,
5+
TooltipContent,
6+
TooltipProvider,
7+
TooltipTrigger,
8+
} from "@/components/ui/tooltip";
9+
10+
interface ButtonWithTooltipProps {
11+
children: React.ReactElement;
12+
side: "top" | "bottom" | "left" | "right";
13+
toolTipText: string;
14+
}
15+
16+
const ButtonWithTooltip = forwardRef<HTMLDivElement, ButtonWithTooltipProps>(
17+
({ children, side, toolTipText }, ref) => {
18+
return (
19+
<TooltipProvider>
20+
<Tooltip delayDuration={0}>
21+
<TooltipTrigger asChild>
22+
{React.cloneElement(children, { ref })}
23+
</TooltipTrigger>
24+
<TooltipContent side={side}>
25+
<div>{toolTipText}</div>
26+
</TooltipContent>
27+
</Tooltip>
28+
</TooltipProvider>
29+
);
30+
}
31+
);
32+
33+
ButtonWithTooltip.displayName = "ButtonWithTooltip";
34+
35+
export default ButtonWithTooltip;

src/components/chat/chat-list.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,22 @@ import {
77
ChatBubbleAvatar,
88
ChatBubbleMessage,
99
} from "../ui/chat/chat-bubble";
10+
import { ChatRequestOptions } from "ai";
1011

1112
interface ChatListProps {
1213
messages: Message[];
1314
isLoading: boolean;
1415
loadingSubmit?: boolean;
16+
reload: (
17+
chatRequestOptions?: ChatRequestOptions
18+
) => Promise<string | null | undefined>;
1519
}
1620

1721
export default function ChatList({
1822
messages,
1923
isLoading,
2024
loadingSubmit,
25+
reload,
2126
}: ChatListProps) {
2227
return (
2328
<div className="flex-1 w-full overflow-y-auto">
@@ -28,6 +33,7 @@ export default function ChatList({
2833
message={message}
2934
isLast={index === messages.length - 1}
3035
isLoading={isLoading}
36+
reload={reload}
3137
/>
3238
))}
3339
{loadingSubmit && (

src/components/chat/chat-message.tsx

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,39 @@ import {
1111
ChatBubbleAvatar,
1212
ChatBubbleMessage,
1313
} from "../ui/chat/chat-bubble";
14+
import ButtonWithTooltip from "../button-with-tooltip";
15+
import { Button } from "../ui/button";
16+
import { CheckIcon, CopyIcon } from "@radix-ui/react-icons";
17+
import { ChatRequestOptions } from "ai";
18+
import { RefreshCcw } from "lucide-react";
1419

1520
export type ChatMessageProps = {
1621
message: Message;
1722
isLast: boolean;
1823
isLoading: boolean | undefined;
24+
reload: (
25+
chatRequestOptions?: ChatRequestOptions
26+
) => Promise<string | null | undefined>;
1927
};
2028

21-
function ChatMessage({ message, isLast, isLoading }: ChatMessageProps) {
29+
function ChatMessage({ message, isLast, isLoading, reload }: ChatMessageProps) {
2230
const contentParts = useMemo(
2331
() => message.content.split("```"),
2432
[message.content]
2533
);
2634

2735
const variant = message.role === "user" ? "sent" : "received";
2836

37+
const [isCopied, setisCopied] = React.useState<boolean>(false);
38+
39+
const copyToClipboard = (response: string) => () => {
40+
navigator.clipboard.writeText(response);
41+
setisCopied(true);
42+
setTimeout(() => {
43+
setisCopied(false);
44+
}, 1500);
45+
};
46+
2947
return (
3048
<motion.div
3149
layout
@@ -82,6 +100,45 @@ function ChatMessage({ message, isLast, isLoading }: ChatMessageProps) {
82100
);
83101
}
84102
})}
103+
104+
{message.role === "assistant" && (
105+
<div>
106+
{/* Action buttons */}
107+
<div className="pt-2 flex gap-1 items-center text-muted-foreground">
108+
{/* Copy button */}
109+
{!isLoading && (
110+
<ButtonWithTooltip side="bottom" toolTipText="Copy">
111+
<Button
112+
onClick={copyToClipboard(message.content)}
113+
variant="ghost"
114+
size="icon"
115+
className="h-4 w-4"
116+
>
117+
{isCopied ? (
118+
<CheckIcon className="w-3.5 h-3.5 transition-all" />
119+
) : (
120+
<CopyIcon className="w-3.5 h-3.5 transition-all" />
121+
)}
122+
</Button>
123+
</ButtonWithTooltip>
124+
)}
125+
126+
{/* Only show regenerate button on the last ai message */}
127+
{!isLoading && isLast && (
128+
<ButtonWithTooltip side="bottom" toolTipText="Regenerate">
129+
<Button
130+
variant="ghost"
131+
size="icon"
132+
className="h-4 w-4"
133+
onClick={() => reload()}
134+
>
135+
<RefreshCcw className="w-3.5 h-3.5 scale-100 transition-all" />
136+
</Button>
137+
</ButtonWithTooltip>
138+
)}
139+
</div>
140+
</div>
141+
)}
85142
</ChatBubbleMessage>
86143
</ChatBubble>
87144
</motion.div>

src/components/chat/chat.tsx

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export default function Chat({ initialMessages, id, isMobile }: ChatProps) {
3030
stop,
3131
setMessages,
3232
setInput,
33+
reload,
3334
} = useChat({
3435
id,
3536
initialMessages,
@@ -40,15 +41,15 @@ export default function Chat({ initialMessages, id, isMobile }: ChatProps) {
4041
},
4142
onFinish: (message) => {
4243
const savedMessages = getMessagesById(id);
43-
console.log(savedMessages);
4444
saveMessages(id, [...savedMessages, message]);
4545
setLoadingSubmit(false);
4646
router.replace(`/c/${id}`);
4747
},
4848
onError: (error) => {
4949
setLoadingSubmit(false);
5050
router.replace("/");
51-
toast.error("An error occurred. Please try again.");
51+
console.error(error.message);
52+
console.error(error.cause);
5253
},
5354
});
5455
const [loadingSubmit, setLoadingSubmit] = React.useState(false);
@@ -103,6 +104,19 @@ export default function Chat({ initialMessages, id, isMobile }: ChatProps) {
103104
setBase64Images(null);
104105
};
105106

107+
const removeLatestMessage = () => {
108+
const updatedMessages = messages.slice(0, -1);
109+
setMessages(updatedMessages);
110+
saveMessages(id, updatedMessages);
111+
return updatedMessages;
112+
};
113+
114+
const handleStop = () => {
115+
stop();
116+
saveMessages(id, [...messages]);
117+
setLoadingSubmit(false);
118+
};
119+
106120
return (
107121
<div className="flex flex-col w-full max-w-3xl h-full">
108122
<ChatTopbar
@@ -129,7 +143,7 @@ export default function Chat({ initialMessages, id, isMobile }: ChatProps) {
129143
handleInputChange={handleInputChange}
130144
handleSubmit={onSubmit}
131145
isLoading={isLoading}
132-
stop={stop}
146+
stop={handleStop}
133147
setInput={setInput}
134148
/>
135149
</div>
@@ -139,13 +153,27 @@ export default function Chat({ initialMessages, id, isMobile }: ChatProps) {
139153
messages={messages}
140154
isLoading={isLoading}
141155
loadingSubmit={loadingSubmit}
156+
reload={async () => {
157+
removeLatestMessage();
158+
159+
const requestOptions: ChatRequestOptions = {
160+
options: {
161+
body: {
162+
selectedModel: selectedModel,
163+
},
164+
},
165+
};
166+
167+
setLoadingSubmit(true);
168+
return reload(requestOptions);
169+
}}
142170
/>
143171
<ChatBottombar
144172
input={input}
145173
handleInputChange={handleInputChange}
146174
handleSubmit={onSubmit}
147175
isLoading={isLoading}
148-
stop={stop}
176+
stop={handleStop}
149177
setInput={setInput}
150178
/>
151179
</>

0 commit comments

Comments
 (0)