Skip to content

Commit c40132a

Browse files
authored
Support different kinds of images (#1116)
1 parent 038cd71 commit c40132a

File tree

4 files changed

+271
-101
lines changed

4 files changed

+271
-101
lines changed

src/LLMProviders/chainRunner.ts

+71-34
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ import {
1515
extractUniqueTitlesFromDocs,
1616
extractYoutubeUrl,
1717
formatDateTime,
18-
imageToBase64,
18+
ImageContent,
19+
ImageProcessor,
20+
MessageContent,
1921
} from "@/utils";
2022
import { Notice } from "obsidian";
2123
import ChainManager from "./chainManager";
@@ -251,6 +253,72 @@ class CopilotPlusChainRunner extends BaseChainRunner {
251253
return hasYoutubeCommand && youtubeUrl !== null && words.length === 1;
252254
}
253255

256+
private async processImageUrls(urls: string[]): Promise<ImageContent[]> {
257+
try {
258+
const imageUrls = await Promise.all(
259+
urls.map(async (url) => {
260+
if (await ImageProcessor.isImageUrl(url)) {
261+
return await ImageProcessor.convertToBase64(url, this.chainManager.app.vault);
262+
}
263+
return null;
264+
})
265+
);
266+
267+
// Filter out null values and return valid image URLs
268+
return imageUrls.filter((item): item is ImageContent => item !== null);
269+
} catch (error) {
270+
console.error("Error processing image URLs:", error);
271+
return [];
272+
}
273+
}
274+
275+
private async processExistingImages(content: MessageContent[]): Promise<ImageContent[]> {
276+
try {
277+
const imageContent = await Promise.all(
278+
content
279+
.filter(
280+
(item): item is ImageContent => item.type === "image_url" && !!item.image_url?.url
281+
)
282+
.map(async (item) => {
283+
return await ImageProcessor.convertToBase64(
284+
item.image_url.url,
285+
this.chainManager.app.vault
286+
);
287+
})
288+
);
289+
return imageContent;
290+
} catch (error) {
291+
console.error("Error processing images:", error);
292+
throw error;
293+
}
294+
}
295+
296+
private async buildMessageContent(
297+
textContent: string,
298+
userMessage: ChatMessage
299+
): Promise<MessageContent[]> {
300+
const content: MessageContent[] = [
301+
{
302+
type: "text",
303+
text: textContent,
304+
},
305+
];
306+
307+
// Process URLs in the message to identify images
308+
if (userMessage.context?.urls && userMessage.context.urls.length > 0) {
309+
const imageContents = await this.processImageUrls(userMessage.context.urls);
310+
content.push(...imageContents);
311+
}
312+
313+
// Add existing image content if present
314+
if (userMessage.content && userMessage.content.length > 0) {
315+
const imageContents = await this.processExistingImages(userMessage.content);
316+
content.push(...imageContents);
317+
}
318+
319+
return content;
320+
}
321+
254322
private async streamMultimodalResponse(
255323
textContent: string,
256324
userMessage: ChatMessage,
@@ -289,39 +357,8 @@ class CopilotPlusChainRunner extends BaseChainRunner {
289357
messages.push({ role: "assistant", content: ai });
290358
}
291359

292-
// Create content array for current message
293-
const content: Array<{ type: string } & ({ text: string } | { image_url: { url: string } })> = [
294-
{
295-
type: "text",
296-
text: textContent,
297-
},
298-
];
299-
300-
// Add image content if present
301-
if (userMessage.content && userMessage.content.length > 0) {
302-
try {
303-
const imageContent = await Promise.all(
304-
userMessage.content
305-
.filter((item) => item.type === "image_url" && item.image_url?.url)
306-
.map(async (item) => {
307-
const base64Image = await imageToBase64(
308-
item.image_url.url,
309-
this.chainManager.app.vault
310-
);
311-
return {
312-
type: "image_url",
313-
image_url: {
314-
url: base64Image,
315-
},
316-
};
317-
})
318-
);
319-
content.push(...imageContent);
320-
} catch (error) {
321-
console.error("Error processing images:", error);
322-
throw error;
323-
}
324-
}
360+
// Build message content with text and images
361+
const content = await this.buildMessageContent(textContent, userMessage);
325362

326363
// Add current user message
327364
messages.push({

src/components/Chat.tsx

+10-2
Original file line numberDiff line numberDiff line change
@@ -165,13 +165,14 @@ const Chat: React.FC<ChatProps> = ({
165165
const urlContextAddition =
166166
currentChain === ChainType.COPILOT_PLUS_CHAIN
167167
? await mention.processUrls(inputMessage || "")
168-
: "";
168+
: { urlContext: "", imageUrls: [] };
169169

170170
// Add context notes
171171
const noteContextAddition = await processContextNotes(customPromptProcessor, fileParserManager);
172172

173173
// Combine everything
174-
processedUserMessage = processedUserMessage + urlContextAddition + noteContextAddition;
174+
processedUserMessage =
175+
processedUserMessage + urlContextAddition.urlContext + noteContextAddition;
175176

176177
let messageWithToolCalls = inputMessage;
177178
// Add tool calls last
@@ -186,6 +187,13 @@ const Chat: React.FC<ChatProps> = ({
186187
isVisible: false,
187188
timestamp: timestamp,
188189
content: content,
190+
context: {
191+
notes,
192+
urls:
193+
currentChain === ChainType.COPILOT_PLUS_CHAIN
194+
? [...(urls || []), ...urlContextAddition.imageUrls]
195+
: urls || [],
196+
},
189197
};
190198

191199
// Add hidden user message to chat history

src/mentions/Mention.ts

+11-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { BrevilabsClient, Url4llmResponse } from "@/LLMProviders/brevilabsClient";
2-
import { isYoutubeUrl } from "@/utils";
2+
import { ImageProcessor, isYoutubeUrl } from "@/utils";
33

44
export interface MentionData {
55
type: string;
@@ -50,17 +50,24 @@ export class Mention {
5050
}
5151

5252
// For non-youtube URLs
53-
async processUrls(text: string): Promise<string> {
53+
async processUrls(text: string): Promise<{ urlContext: string; imageUrls: string[] }> {
5454
const urls = this.extractUrls(text);
5555
let urlContext = "";
56+
const imageUrls: string[] = [];
5657

5758
// Return empty string if no URLs to process
5859
if (urls.length === 0) {
59-
return "";
60+
return { urlContext: "", imageUrls: [] };
6061
}
6162

6263
// Process all URLs concurrently
6364
const processPromises = urls.map(async (url) => {
65+
// Check if it's an image URL
66+
if (await ImageProcessor.isImageUrl(url)) {
67+
imageUrls.push(url);
68+
return null;
69+
}
70+
6471
if (!this.mentions.has(url)) {
6572
const processed = await this.processUrl(url);
6673
this.mentions.set(url, {
@@ -81,7 +88,7 @@ export class Mention {
8188
}
8289
});
8390

84-
return urlContext;
91+
return { urlContext, imageUrls };
8592
}
8693

8794
getMentions(): Map<string, MentionData> {

0 commit comments

Comments
 (0)