Skip to content

Commit 8b16cc6

Browse files
connellr023erinakibriagithub-actions[bot]
authored
Prompt images (#25)
* image upload feature mostly finished * Updated UI * Fixed backend logic error + frontend lint issue * fix: apply frontend code formatting fixes --------- Co-authored-by: Erina <erinakibria@gmail.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent c6ec04a commit 8b16cc6

File tree

13 files changed

+169
-37
lines changed

13 files changed

+169
-37
lines changed

configs/models.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
llama3.2:1b
2-
qwen2.5:0.5b
1+
qwen2.5:0.5b
2+
llava-llama3:8b

internal/controllers/post_conversation_chat.go

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ import (
1717
)
1818

1919
type postConversationChatRequest struct {
20-
Prompt string `json:"prompt"`
21-
Model string `json:"model"`
22-
ConversationID string `json:"conversationId"`
20+
Prompt string `json:"prompt"`
21+
Images []string `json:"images"`
22+
Model string `json:"model"`
23+
ConversationID string `json:"conversationId"`
2324
}
2425

2526
func (i *Injection) PostConversationChat(w http.ResponseWriter, r *http.Request) {
@@ -59,7 +60,7 @@ func (i *Injection) PostConversationChat(w http.ResponseWriter, r *http.Request)
5960
// Construct request to Ollama API
6061
ollamaReq := models.OllamaChatRequest{
6162
Model: req.Model,
62-
Messages: utils.BuildOllamaMessages(systemPrompt, req.Prompt, prevChatRecords),
63+
Messages: utils.BuildOllamaMessages(systemPrompt, req.Prompt, req.Images, prevChatRecords),
6364
Stream: true,
6465
}
6566

@@ -110,10 +111,6 @@ func (i *Injection) PostConversationChat(w http.ResponseWriter, r *http.Request)
110111
return
111112
}
112113

113-
// Images for now
114-
// TODO
115-
promptImages := []string{}
116-
117114
// Aggregate saving the chat record in the database and cache
118115
wg := sync.WaitGroup{}
119116
wg.Add(2)
@@ -123,7 +120,7 @@ func (i *Injection) PostConversationChat(w http.ResponseWriter, r *http.Request)
123120
go func() {
124121
defer wg.Done()
125122

126-
if _, err := db.SaveChatRecord(i.Sdb, req.Prompt, promptImages, replyBuilder.String(), creationTime, user.ID, req.ConversationID); err != nil {
123+
if _, err := db.SaveChatRecord(i.Sdb, req.Prompt, req.Images, replyBuilder.String(), creationTime, user.ID, req.ConversationID); err != nil {
127124
errorChan <- err
128125
}
129126
}()
@@ -135,7 +132,7 @@ func (i *Injection) PostConversationChat(w http.ResponseWriter, r *http.Request)
135132
record := models.ClientChatRecord{
136133
CreatedAt: creationTime,
137134
Prompt: req.Prompt,
138-
PromptImages: promptImages,
135+
PromptImages: req.Images,
139136
Reply: replyBuilder.String(),
140137
}
141138

internal/controllers/post_guest_chat.go

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ import (
1414
)
1515

1616
type postGuestChatRequest struct {
17-
Prompt string `json:"prompt"`
18-
Model string `json:"model"`
17+
Prompt string `json:"prompt"`
18+
Images []string `json:"images"`
19+
Model string `json:"model"`
1920
}
2021

2122
// Endpoint for sending a prompt on a guest chat.
@@ -57,7 +58,7 @@ func (i *Injection) PostGuestChat(w http.ResponseWriter, r *http.Request) {
5758
// Construct request to Ollama API.
5859
ollamaReq := models.OllamaChatRequest{
5960
Model: req.Model,
60-
Messages: utils.BuildOllamaMessages(systemPrompt, req.Prompt, prevChatRecords),
61+
Messages: utils.BuildOllamaMessages(systemPrompt, req.Prompt, req.Images, prevChatRecords),
6162
Stream: true,
6263
}
6364

@@ -108,15 +109,11 @@ func (i *Injection) PostGuestChat(w http.ResponseWriter, r *http.Request) {
108109
return
109110
}
110111

111-
// Images for now.
112-
// TODO.
113-
promptImages := []string{}
114-
115112
// Cache chat in Redis.
116113
record := models.ClientChatRecord{
117114
CreatedAt: creationTime,
118115
Prompt: req.Prompt,
119-
PromptImages: promptImages,
116+
PromptImages: req.Images,
120117
Reply: replyBuilder.String(),
121118
}
122119

internal/utils/build_ollama_messages.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import "tokenbase/internal/models"
1212
//
1313
// Returns:
1414
// - A list of Ollama compatible chat messages.
15-
func BuildOllamaMessages(systemPrompt string, newPrompt string, records []models.ClientChatRecord) []models.OllamaChatMessage {
15+
func BuildOllamaMessages(systemPrompt string, newPrompt string, newImages []string, records []models.ClientChatRecord) []models.OllamaChatMessage {
1616
// Allocate enough space for the messages.
1717
// 2 messages per record + 2 messages for the system prompt and the new prompt.
1818
n := (len(records) * 2) + 2
@@ -39,6 +39,7 @@ func BuildOllamaMessages(systemPrompt string, newPrompt string, records []models
3939
newMessage := models.OllamaChatMessage{
4040
Role: models.UserRole,
4141
Content: newPrompt,
42+
Images: newImages,
4243
}
4344

4445
messages = append(messages, newMessage)

web/src/components/Chat.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,18 @@ import ButtonColor from "@/models/ButtonColor";
33
import IconButton from "./IconButton";
44
import TypesetRenderer from "./TypesetRenderer";
55
import TypeCursor from "./TypeCursor";
6-
import { faCopy, faTrash } from "@fortawesome/free-solid-svg-icons";
76
import { useState } from "react";
7+
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
8+
import {
9+
faCopy,
10+
faPaperclip,
11+
faTrash,
12+
} from "@fortawesome/free-solid-svg-icons";
813

914
type ChatProps = {
1015
createdAt: number;
1116
prompt: string;
17+
images: string[];
1218
reply: string;
1319
isComplete: boolean;
1420
onDelete?: (createdAt: number) => void;
@@ -17,6 +23,7 @@ type ChatProps = {
1723
const Chat: React.FC<ChatProps> = ({
1824
createdAt,
1925
prompt,
26+
images,
2027
reply,
2128
isComplete,
2229
onDelete,
@@ -35,6 +42,12 @@ const Chat: React.FC<ChatProps> = ({
3542
return (
3643
<div className={styles.container}>
3744
<div className={styles.promptContainer}>
45+
{images.length > 0 && (
46+
<div className={styles.imageCount}>
47+
<FontAwesomeIcon icon={faPaperclip} />
48+
{images.length}
49+
</div>
50+
)}
3851
<TypesetRenderer>{prompt}</TypesetRenderer>
3952
</div>
4053
{reply.length > 0 && (

web/src/components/ChatContainer.tsx

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ type ChatContainerProps = {
2323
promptEndpoint: string;
2424
deleteEndpoint: string;
2525
suggestions: string[];
26-
constructPromptRequest: (prompt: string) => Promise<Result<HttpChatReq>>;
26+
constructPromptRequest: (
27+
prompt: string,
28+
promptImages: string[],
29+
) => Promise<Result<HttpChatReq>>;
2730
constructDeleteRequest: (createdAt: number) => Promise<Result<HttpChatReq>>;
2831
};
2932

@@ -43,13 +46,14 @@ const ChatContainer: React.FC<ChatContainerProps> = ({
4346
const loadingIndicatorRef = useRef<HTMLDivElement | null>(null);
4447

4548
const onPromptSend = useCallback(
46-
async (prompt: string) => {
49+
async (prompt: string, promptImages: string[]) => {
4750
// Clear any previous error
4851
setError(null);
4952

5053
// Initialize a new chat entry
5154
let newChat: ChatRecord = {
52-
prompt: prompt,
55+
prompt,
56+
promptImages,
5357
reply: "",
5458
};
5559

@@ -58,7 +62,11 @@ const ChatContainer: React.FC<ChatContainerProps> = ({
5862
setStreamingChat(newChat);
5963

6064
// Construct the request using the provided callback
61-
const { ok, error } = await constructPromptRequest(prompt);
65+
66+
const { ok, error } = await constructPromptRequest(
67+
prompt,
68+
promptImages.map((img) => img.substring(img.search(/,/) + 1)),
69+
);
6270

6371
if (error) {
6472
setError(error);
@@ -197,7 +205,10 @@ const ChatContainer: React.FC<ChatContainerProps> = ({
197205
<div className={styles.suggestionsContainer}>
198206
{/* Render some random chat suggestions */}
199207
{suggestions?.map((suggestion, i) => (
200-
<StandardButton key={i} onClick={() => onPromptSend(suggestion)}>
208+
<StandardButton
209+
key={i}
210+
onClick={() => onPromptSend(suggestion, [])}
211+
>
201212
{suggestion}
202213
</StandardButton>
203214
))}
@@ -212,6 +223,7 @@ const ChatContainer: React.FC<ChatContainerProps> = ({
212223
key={i}
213224
createdAt={chat.createdAt ?? -1}
214225
prompt={chat.prompt}
226+
images={chat.promptImages ?? []}
215227
reply={chat.reply}
216228
isComplete={true}
217229
onDelete={onChatDelete}
@@ -223,6 +235,7 @@ const ChatContainer: React.FC<ChatContainerProps> = ({
223235
<Chat
224236
createdAt={streamingChat.createdAt ?? -1}
225237
prompt={streamingChat.prompt}
238+
images={streamingChat.promptImages ?? []}
226239
reply={streamingChat.reply}
227240
isComplete={false}
228241
/>

web/src/components/GuestChat.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ const GuestChat: React.FC<GuestChatProps> = ({ chatSuggestions }) => {
2626
const { bearer, setBearer } = useBearerContext();
2727
const { availableModels, selectedModelIndex } = useModelsContext();
2828

29-
const constructGuestPromptRequest = async (prompt: string) => {
29+
const constructGuestPromptRequest = async (
30+
prompt: string,
31+
images: string[],
32+
) => {
3033
const constructReq = (token: string) => {
3134
if (availableModels.length === 0) {
3235
return {
@@ -41,6 +44,7 @@ const GuestChat: React.FC<GuestChatProps> = ({ chatSuggestions }) => {
4144
},
4245
body: JSON.stringify({
4346
prompt,
47+
images,
4448
model: availableModels[selectedModelIndex].tag,
4549
}),
4650
};

web/src/components/PromptArea.tsx

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import styles from "@/styles/components/PromptArea.module.scss";
22
import { useState, useRef, useEffect, useCallback } from "react";
33
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
4-
import { merriweather500 } from "@/utils/fonts";
4+
import { merriweather400, merriweather500 } from "@/utils/fonts";
55
import {
66
faArrowUp,
77
faPaperclip,
@@ -12,7 +12,7 @@ type PromptAreaProps = {
1212
isDisabled: boolean;
1313
canCancel: boolean;
1414
canAttach: boolean;
15-
onSend: (prompt: string) => void;
15+
onSend: (prompt: string, promptImages: string[]) => void;
1616
onCancel: () => void;
1717
};
1818

@@ -24,16 +24,19 @@ const PromptArea: React.FC<PromptAreaProps> = ({
2424
onCancel,
2525
}) => {
2626
const [prompt, setPrompt] = useState("");
27+
const [attachedImages, setAttachedImages] = useState<string[]>([]);
2728
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
2829

2930
const handleSend = () => {
30-
onSend(prompt.trim());
31+
onSend(prompt.trim(), attachedImages);
32+
setAttachedImages([]);
3133
setPrompt("");
3234
};
3335

3436
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
3537
if (e.key === "Enter" && !e.shiftKey) {
3638
e.preventDefault();
39+
3740
if (prompt.trim().length > 0 && !isDisabled) {
3841
handleSend();
3942
}
@@ -44,6 +47,33 @@ const PromptArea: React.FC<PromptAreaProps> = ({
4447
setPrompt(e.target.value);
4548
};
4649

50+
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
51+
const files = e.target.files;
52+
53+
if (files) {
54+
const uploadedImages: string[] = [];
55+
56+
Array.from(files).forEach((file) => {
57+
const reader = new FileReader();
58+
reader.onload = (event) => {
59+
const target = event.target;
60+
61+
if (target === null) {
62+
return;
63+
}
64+
65+
uploadedImages.push(target.result as string);
66+
setAttachedImages((prev) => [...prev, target.result as string]);
67+
};
68+
69+
reader.readAsDataURL(file);
70+
});
71+
}
72+
73+
// Reset the file input
74+
e.target.value = "";
75+
};
76+
4777
const adjustTextareaHeight = useCallback(() => {
4878
if (textareaRef.current) {
4979
textareaRef.current.style.height = "auto";
@@ -77,8 +107,22 @@ const PromptArea: React.FC<PromptAreaProps> = ({
77107
spellCheck={false}
78108
/>
79109
<div className={styles.buttonContainer}>
80-
<button disabled={isDisabled || !canAttach}>
110+
<button type="button" disabled={isDisabled || !canAttach}>
111+
{attachedImages.length > 0 && (
112+
<span
113+
className={`${styles.imageCount} ${merriweather400.className}`}
114+
>
115+
{Math.min(attachedImages.length, 99)}
116+
</span>
117+
)}
81118
<FontAwesomeIcon icon={faPaperclip} />
119+
<input
120+
id="file-upload"
121+
type="file"
122+
multiple
123+
accept="image/*"
124+
onChange={handleImageUpload}
125+
/>
82126
</button>
83127
{canCancel ? (
84128
<button onClick={onCancel}>

web/src/components/UserChat.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ const UserChat: React.FC<UserChatProps> = ({ chatSuggestions }) => {
2828
throw new Error("Bearer token is not available");
2929
}
3030

31-
const constructUserPromptRequest = async (prompt: string) => {
31+
const constructUserPromptRequest = async (
32+
prompt: string,
33+
images: string[],
34+
) => {
3235
if (conversationRecords === null) {
3336
return {
3437
error: "Cannot create prompt request without conversation records",
@@ -49,6 +52,7 @@ const UserChat: React.FC<UserChatProps> = ({ chatSuggestions }) => {
4952
},
5053
body: JSON.stringify({
5154
prompt,
55+
images,
5256
model: availableModels[selectedModelIndex].tag,
5357
conversationId,
5458
}),
@@ -60,6 +64,7 @@ const UserChat: React.FC<UserChatProps> = ({ chatSuggestions }) => {
6064
const newConversation = await reqNewConversation(
6165
availableModels[selectedModelIndex].tag,
6266
prompt,
67+
images,
6368
bearer.token,
6469
);
6570

web/src/pages/_app.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@ class RootApp extends App<RootAppProps> {
2121
static async getInitialProps(appContext: AppContext) {
2222
const appProps = await App.getInitialProps(appContext);
2323

24+
// Only fetch available models on the home page
2425
return {
25-
availableModels: await getAvailableModels(),
26+
availableModels:
27+
appContext.router.pathname === "/" ? await getAvailableModels() : [],
2628
...appProps,
2729
};
2830
}

0 commit comments

Comments
 (0)