Skip to content

Commit e96cfbc

Browse files
authored
Merge pull request #77 from jakobhoeg/development
feat: add vision support (dev env. support only for now)
2 parents 72af508 + aeaa935 commit e96cfbc

File tree

9 files changed

+3105
-2669
lines changed

9 files changed

+3105
-2669
lines changed

package-lock.json

Lines changed: 2783 additions & 2522 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 43 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -9,56 +9,59 @@
99
"lint": "next lint"
1010
},
1111
"dependencies": {
12-
"@emoji-mart/data": "^1.1.2",
12+
"@emoji-mart/data": "^1.2.1",
1313
"@emoji-mart/react": "^1.1.1",
14-
"@hookform/resolvers": "^3.3.4",
15-
"@langchain/community": "^0.0.26",
16-
"@langchain/core": "^0.1.35",
17-
"@radix-ui/react-avatar": "^1.0.4",
18-
"@radix-ui/react-dialog": "^1.0.5",
19-
"@radix-ui/react-dropdown-menu": "^2.0.6",
14+
"@hookform/resolvers": "^3.9.0",
15+
"@langchain/community": "^0.3.1",
16+
"@langchain/core": "^0.3.3",
17+
"@radix-ui/react-avatar": "^1.1.0",
18+
"@radix-ui/react-dialog": "^1.1.1",
19+
"@radix-ui/react-dropdown-menu": "^2.1.1",
2020
"@radix-ui/react-icons": "^1.3.0",
21-
"@radix-ui/react-label": "^2.0.2",
22-
"@radix-ui/react-popover": "^1.0.7",
23-
"@radix-ui/react-scroll-area": "^1.0.5",
24-
"@radix-ui/react-select": "^2.0.0",
25-
"@radix-ui/react-slot": "^1.0.2",
26-
"@radix-ui/react-tooltip": "^1.0.7",
21+
"@radix-ui/react-label": "^2.1.0",
22+
"@radix-ui/react-popover": "^1.1.1",
23+
"@radix-ui/react-scroll-area": "^1.1.0",
24+
"@radix-ui/react-select": "^2.1.1",
25+
"@radix-ui/react-slot": "^1.1.0",
26+
"@radix-ui/react-tooltip": "^1.1.2",
2727
"@types/dom-speech-recognition": "^0.0.4",
28-
"ai": "^2.2.37",
28+
"ai": "^3.4.0",
2929
"class-variance-authority": "^0.7.0",
30-
"clsx": "^2.1.0",
31-
"emoji-mart": "^5.5.2",
32-
"framer-motion": "^11.0.3",
33-
"langchain": "^0.1.13",
34-
"lucide-react": "^0.322.0",
35-
"next": "^14.2.3",
36-
"next-themes": "^0.2.1",
37-
"react": "^18",
30+
"clsx": "^2.1.1",
31+
"emoji-mart": "^5.6.0",
32+
"framer-motion": "^11.5.6",
33+
"langchain": "^0.3.2",
34+
"lucide-react": "^0.445.0",
35+
"next": "^14.2.13",
36+
"next-themes": "^0.3.0",
37+
"ollama-ai-provider": "^0.15.0",
38+
"react": "^18.3.1",
3839
"react-code-blocks": "^0.1.6",
39-
"react-dom": "^18",
40-
"react-hook-form": "^7.50.1",
40+
"react-dom": "^18.3.1",
41+
"react-dropzone": "^14.2.9",
42+
"react-hook-form": "^7.53.0",
4143
"react-markdown": "^9.0.1",
42-
"react-resizable-panels": "^2.0.3",
44+
"react-resizable-panels": "^2.1.3",
4345
"react-textarea-autosize": "^8.5.3",
4446
"remark-gfm": "^4.0.0",
45-
"sharp": "^0.33.4",
46-
"sonner": "^1.4.0",
47-
"tailwind-merge": "^2.2.1",
47+
"sharp": "^0.33.5",
48+
"sonner": "^1.5.0",
49+
"tailwind-merge": "^2.5.2",
4850
"tailwindcss-animate": "^1.0.7",
49-
"uuid": "^9.0.1",
50-
"zod": "^3.22.4"
51+
"uuid": "^10.0.0",
52+
"zod": "^3.23.8",
53+
"zustand": "^5.0.0-rc.2"
5154
},
5255
"devDependencies": {
53-
"@types/node": "^20",
54-
"@types/react": "^18",
55-
"@types/react-dom": "^18",
56-
"@types/uuid": "^9.0.8",
57-
"autoprefixer": "^10.0.1",
58-
"eslint": "^8",
59-
"eslint-config-next": "14.1.0",
60-
"postcss": "^8",
61-
"tailwindcss": "^3.3.0",
62-
"typescript": "^5"
56+
"@types/node": "^22.5.5",
57+
"@types/react": "^18.3.8",
58+
"@types/react-dom": "^18.3.0",
59+
"@types/uuid": "^10.0.0",
60+
"autoprefixer": "^10.4.20",
61+
"eslint": "^8.0.0",
62+
"eslint-config-next": "14.2.13",
63+
"postcss": "^8.4.47",
64+
"tailwindcss": "^3.4.12",
65+
"typescript": "^5.6.2"
6366
}
6467
}

src/app/[id]/page.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ import { getSelectedModel } from "@/lib/model-helper";
55
import { ChatOllama } from "@langchain/community/chat_models/ollama";
66
import { AIMessage, HumanMessage } from "@langchain/core/messages";
77
import { BytesOutputParser } from "@langchain/core/output_parsers";
8-
import { ChatRequestOptions } from "ai";
8+
import { Attachment, ChatRequestOptions } from "ai";
99
import { Message, useChat } from "ai/react";
1010
import React, { useEffect } from "react";
1111
import { toast } from "sonner";
1212
import { v4 as uuidv4 } from "uuid";
13+
import useChatStore from "../hooks/useChatStore";
1314

1415
export default function Page({ params }: { params: { id: string } }) {
1516
const {
@@ -40,6 +41,9 @@ export default function Page({ params }: { params: { id: string } }) {
4041
const [ollama, setOllama] = React.useState<ChatOllama>();
4142
const env = process.env.NODE_ENV;
4243
const [loadingSubmit, setLoadingSubmit] = React.useState(false);
44+
const formRef = React.useRef<HTMLFormElement>(null);
45+
const base64Images = useChatStore((state) => state.base64Images);
46+
const setBase64Images = useChatStore((state) => state.setBase64Images);
4347

4448
useEffect(() => {
4549
if (env === "production") {
@@ -120,21 +124,36 @@ export default function Page({ params }: { params: { id: string } }) {
120124

121125
setMessages([...messages]);
122126

127+
const attachments: Attachment[] = base64Images
128+
? base64Images.map((image) => ({
129+
contentType: 'image/base64', // Content type for base64 images
130+
url: image, // The base64 image data
131+
}))
132+
: [];
133+
123134
// Prepare the options object with additional body data, to pass the model.
124135
const requestOptions: ChatRequestOptions = {
125136
options: {
126137
body: {
127138
selectedModel: selectedModel,
128139
},
129140
},
141+
...(base64Images && {
142+
data: {
143+
images: base64Images,
144+
},
145+
experimental_attachments: attachments
146+
}),
130147
};
131148

132149
if (env === "production" && selectedModel !== "REST API") {
133150
handleSubmitProduction(e);
151+
setBase64Images(null)
134152
} else {
135153
// use the /api/chat route
136154
// Call the handleSubmit function with the options
137155
handleSubmit(e, requestOptions);
156+
setBase64Images(null)
138157
}
139158
};
140159

@@ -162,6 +181,7 @@ export default function Page({ params }: { params: { id: string } }) {
162181
stop={stop}
163182
navCollapsedSize={10}
164183
defaultLayout={[30, 160]}
184+
formRef={formRef}
165185
setMessages={setMessages}
166186
setInput={setInput}
167187
/>

src/app/api/chat/route.ts

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,35 @@
1-
import { StreamingTextResponse, Message } from "ai";
2-
import { ChatOllama } from "@langchain/community/chat_models/ollama";
3-
import { AIMessage, HumanMessage } from "@langchain/core/messages";
4-
import { BytesOutputParser } from "@langchain/core/output_parsers";
1+
import { createOllama } from 'ollama-ai-provider';
2+
import { streamText, convertToCoreMessages, CoreMessage, UserContent } from 'ai';
53

64
export const runtime = "edge";
75
export const dynamic = "force-dynamic";
86

97
export async function POST(req: Request) {
10-
const { messages, selectedModel } = await req.json();
8+
// Destructure request data
9+
const { messages, selectedModel, data } = await req.json();
1110

12-
const model = new ChatOllama({
13-
baseUrl: process.env.NEXT_PUBLIC_OLLAMA_URL || "http://localhost:11434",
14-
model: selectedModel,
15-
});
11+
const initialMessages = messages.slice(0, -1);
12+
const currentMessage = messages[messages.length - 1];
13+
14+
const ollama = createOllama({});
1615

17-
const parser = new BytesOutputParser();
16+
// Build message content array directly
17+
const messageContent: UserContent = [{ type: 'text', text: currentMessage.content }];
1818

19-
const stream = await model
20-
.pipe(parser)
21-
.stream(
22-
(messages as Message[]).map((m) =>
23-
m.role == "user"
24-
? new HumanMessage(m.content)
25-
: new AIMessage(m.content)
26-
)
27-
);
19+
// Add images if they exist
20+
data?.images?.forEach((imageUrl: string) => {
21+
const image = new URL(imageUrl);
22+
messageContent.push({ type: 'image', image });
23+
});
2824

25+
// Stream text using the ollama model
26+
const result = await streamText({
27+
model: ollama(selectedModel),
28+
messages: [
29+
...convertToCoreMessages(initialMessages),
30+
{ role: 'user', content: messageContent },
31+
],
32+
});
2933

30-
return new StreamingTextResponse(stream);
34+
return result.toDataStreamResponse();
3135
}

src/app/hooks/useChatStore.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
2+
import { CoreMessage } from "ai";
3+
import { create } from "zustand";
4+
5+
interface State {
6+
base64Images: string[] | null;
7+
messages: CoreMessage[];
8+
}
9+
10+
interface Actions {
11+
setBase64Images: (base64Images: string[] | null) => void;
12+
setMessages: (
13+
fn: (
14+
messages: CoreMessage[]
15+
) => CoreMessage[]
16+
) => void;
17+
}
18+
19+
const useChatStore = create<State & Actions>()(
20+
(set) => ({
21+
base64Images: null,
22+
setBase64Images: (base64Images) => set({ base64Images }),
23+
24+
messages: [],
25+
setMessages: (fn) => set((state) => ({ messages: fn(state.messages) })),
26+
})
27+
)
28+
29+
export default useChatStore;

src/app/page.tsx

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,12 @@ import { getSelectedModel } from "@/lib/model-helper";
1515
import { ChatOllama } from "@langchain/community/chat_models/ollama";
1616
import { AIMessage, HumanMessage } from "@langchain/core/messages";
1717
import { BytesOutputParser } from "@langchain/core/output_parsers";
18-
import { ChatRequestOptions } from "ai";
18+
import { Attachment, ChatRequestOptions } from "ai";
1919
import { Message, useChat } from "ai/react";
2020
import React, { useEffect, useRef, useState } from "react";
2121
import { toast } from "sonner";
2222
import { v4 as uuidv4 } from "uuid";
23+
import useChatStore from "./hooks/useChatStore";
2324

2425
export default function Home() {
2526
const {
@@ -29,6 +30,7 @@ export default function Home() {
2930
handleSubmit,
3031
isLoading,
3132
error,
33+
data,
3234
stop,
3335
setMessages,
3436
setInput,
@@ -52,6 +54,8 @@ export default function Home() {
5254
const env = process.env.NODE_ENV;
5355
const [loadingSubmit, setLoadingSubmit] = React.useState(false);
5456
const formRef = useRef<HTMLFormElement>(null);
57+
const base64Images = useChatStore((state) => state.base64Images);
58+
const setBase64Images = useChatStore((state) => state.setBase64Images);
5559

5660
useEffect(() => {
5761
if (messages.length < 1) {
@@ -85,7 +89,7 @@ export default function Home() {
8589
}
8690
}, [selectedModel]);
8791

88-
const addMessage = (Message: any) => {
92+
const addMessage = (Message: Message) => {
8993
messages.push(Message);
9094
window.dispatchEvent(new Event("storage"));
9195
setMessages([...messages]);
@@ -145,20 +149,38 @@ export default function Home() {
145149

146150
setMessages([...messages]);
147151

152+
const attachments: Attachment[] = base64Images
153+
? base64Images.map((image) => ({
154+
contentType: 'image/base64', // Content type for base64 images
155+
url: image, // The base64 image data
156+
}))
157+
: [];
158+
148159
// Prepare the options object with additional body data, to pass the model.
149160
const requestOptions: ChatRequestOptions = {
150161
options: {
151162
body: {
152163
selectedModel: selectedModel,
153164
},
154165
},
166+
...(base64Images && {
167+
data: {
168+
images: base64Images,
169+
},
170+
experimental_attachments: attachments
171+
}),
155172
};
156173

174+
messages.slice(0, -1)
175+
176+
157177
if (env === "production") {
158178
handleSubmitProduction(e);
179+
setBase64Images(null)
159180
} else {
160181
// Call the handleSubmit function with the options
161182
handleSubmit(e, requestOptions);
183+
setBase64Images(null)
162184
}
163185
};
164186

0 commit comments

Comments
 (0)