feat(chat): add optional photo upload support

Add vision/multimodal support to chat, allowing users to send images
alongside or instead of text prompts. Images are transmitted and persisted
as base64 data URLs.

Backend:
- Add Images []string to Message struct for persistence
- Add Images []string to GenerateTextRequest with relaxed validation
- Build multimodal user messages using OpenAI SDK content parts
- Pass images through from handlers to client
- Deep-copy Images slice in message cloning

Frontend:
- Add images?: string[] to Message and GenerateTextRequest types
- Add image selection state and file input handler
- Add camera icon button, hidden file input, and image preview strip
- Render images in user message bubbles
- Pass images through to GenerateTextRequest

Tests:
- Add TestSendMessageWithImage for vision model testing
This commit is contained in:
2026-05-01 18:27:09 -04:00
parent c51c0ab070
commit e60b1ea8d5
9 changed files with 150 additions and 13 deletions

View File

@@ -37,6 +37,7 @@ Alpine.data('chatManager', () => ({
selectedModel: '',
inputMessage: '',
selectedImages: [] as string[],
error: '',
selectedChatID: null as string | null,
@@ -88,10 +89,12 @@ Alpine.data('chatManager', () => ({
async sendMessage() {
const message = this.inputMessage.trim();
if (!message || this.loading) return;
if ((!message && this.selectedImages.length === 0) || this.loading) return;
// Update State
const images = [...this.selectedImages];
this.inputMessage = '';
this.selectedImages = [];
this.loading = true;
this.error = '';
@@ -121,6 +124,7 @@ Alpine.data('chatManager', () => ({
role: 'user',
thinking: '',
content: message,
images: images,
created_at: new Date().toISOString(),
});
currentChat.message_count += 1;
@@ -128,7 +132,7 @@ Alpine.data('chatManager', () => ({
try {
await sendMessage(
this.selectedChatID === IN_PROGRESS_UUID ? '' : this.selectedChatID,
{ model: this.selectedModel, prompt: message },
{ model: this.selectedModel, prompt: message, images },
(chunk: MessageChunk) => {
if (chunk.chat) this.activeStreamChatID = chunk.chat.id;
this.applyMessageChunk(chunk);

View File

@@ -16,6 +16,7 @@ export interface Message {
role: 'user' | 'assistant';
thinking: string;
content: string;
images?: string[];
status?: MessageStatus;
stats?: MessageStats;
}
@@ -69,6 +70,7 @@ export interface GenerateImageRequest {
export interface GenerateTextRequest {
model: string;
prompt: string;
images?: string[];
}
export interface ChatListResponse {