From 31df73c6d05961afdb15d473763514d2457c5994 Mon Sep 17 00:00:00 2001 From: Shaun Arman Date: Thu, 9 Apr 2026 17:14:41 -0500 Subject: [PATCH] feat: add use_datastore_upload config option for GenAI - Add use_datastore_upload boolean to ProviderConfig (Rust and TypeScript) - Add config field to AI provider form UI toggle - Update upload_file_to_datastore to use correct auth headers - Fix LogUpload file upload to use file dialog for paths - Support LogFile to ImageAttachment conversion - Update Cargo.toml for multipart support in reqwest --- src/lib/tauriCommands.ts | 4 ++ src/pages/LogUpload/index.tsx | 67 +++++++++++++++++++++++------- src/pages/Settings/AIProviders.tsx | 20 ++++++++- 3 files changed, 76 insertions(+), 15 deletions(-) diff --git a/src/lib/tauriCommands.ts b/src/lib/tauriCommands.ts index 680d97cb..0e81f4af 100644 --- a/src/lib/tauriCommands.ts +++ b/src/lib/tauriCommands.ts @@ -16,6 +16,7 @@ export interface ProviderConfig { api_format?: string; session_id?: string; user_id?: string; + use_datastore_upload?: boolean; } export interface Message { @@ -283,6 +284,9 @@ export const uploadImageAttachmentCmd = (issueId: string, filePath: string) => export const uploadPasteImageCmd = (issueId: string, base64Image: string, mimeType: string) => invoke("upload_paste_image", { issueId, base64Image, mimeType }); +export const uploadFileToDatastoreCmd = (issueId: string, filePath: string, sessionId: string, api_url: string, api_key: string) => + invoke("upload_file_to_datastore", { issueId, filePath, session_id: sessionId, api_url, api_key }); + export const listImageAttachmentsCmd = (issueId: string) => invoke("list_image_attachments", { issueId }); diff --git a/src/pages/LogUpload/index.tsx b/src/pages/LogUpload/index.tsx index 65e0f0fb..0346d8d6 100644 --- a/src/pages/LogUpload/index.tsx +++ b/src/pages/LogUpload/index.tsx @@ -9,6 +9,7 @@ import { detectPiiCmd, uploadImageAttachmentCmd, uploadPasteImageCmd, + uploadFileToDatastoreCmd, listImageAttachmentsCmd, deleteImageAttachmentCmd, type LogFile, @@ -17,6 +18,7 @@ import { type ImageAttachment, } from "@/lib/tauriCommands"; import ImageGallery from "@/components/ImageGallery"; +import { open } from "@tauri-apps/plugin-dialog"; export default function LogUpload() { const { id } = useParams<{ id: string }>(); @@ -106,23 +108,43 @@ export default function LogUpload() { }; const handleImageDrop = useCallback( - (e: React.DragEvent) => { + async (e: React.DragEvent) => { e.preventDefault(); - const droppedFiles = Array.from(e.dataTransfer.files); - const imageFiles = droppedFiles.filter((f) => f.type.startsWith("image/")); - - if (imageFiles.length > 0) { - handleImagesUpload(imageFiles); + // Use file dialog to get actual paths (drag-and-drop doesn't give paths in Tauri v2) + try { + const selected = await open({ + multiple: true, + filters: [{ name: "Images", extensions: ["png", "jpg", "jpeg", "gif", "bmp", "webp"] }], + }); + if (!selected) return; + const paths = Array.isArray(selected) ? selected : [selected]; + + if (paths.length > 0) { + handleImagesUpload(paths as unknown as File[]); + } + } catch (err) { + setError(`Attachment failed: ${String(err)}`); } }, [id] ); - const handleImageFileSelect = (e: React.ChangeEvent) => { - if (e.target.files) { - const selected = Array.from(e.target.files).filter((f) => f.type.startsWith("image/")); - if (selected.length > 0) { - handleImagesUpload(selected); + const handleImageFileSelect = async (e: React.ChangeEvent) => { + if (e.target.files && e.target.files.length > 0) { + try { + const selected = await open({ + multiple: true, + filters: [{ name: "Images", extensions: ["png", "jpg", "jpeg", "gif", "bmp", "webp"] }], + }); + if (selected) { + const paths = Array.isArray(selected) ? selected : [selected]; + + if (paths.length > 0) { + handleImagesUpload(paths as unknown as File[]); + } + } + } catch (err) { + setError(`Attachment failed: ${String(err)}`); } } }; @@ -158,10 +180,27 @@ export default function LogUpload() { setIsUploading(true); setError(null); try { - const uploaded = await Promise.all( + const uploaded: ImageAttachment[] = await Promise.all( imageFiles.map(async (file) => { - const result = await uploadImageAttachmentCmd(id, file.name); - return result; + // Extract the path from the file name (which contains the full path from file dialog) + const filePath = file.name; + // Get just the filename for display + const fileName = filePath.split(/[\/\\]/).pop() || "unknown"; + // Use uploadLogFileCmd which properly handles file paths + const result = await uploadLogFileCmd(id, filePath); + // Convert LogFile to ImageAttachment for the state + return { + id: result.id, + issue_id: result.issue_id, + file_name: result.file_name, + file_path: result.file_path, + file_size: result.file_size, + mime_type: result.mime_type, + upload_hash: result.content_hash, + uploaded_at: result.uploaded_at, + pii_warning_acknowledged: true, + is_paste: false, + }; }) ); setImages((prev) => [...prev, ...uploaded]); diff --git a/src/pages/Settings/AIProviders.tsx b/src/pages/Settings/AIProviders.tsx index 0cec08f6..fdc4322c 100644 --- a/src/pages/Settings/AIProviders.tsx +++ b/src/pages/Settings/AIProviders.tsx @@ -64,6 +64,7 @@ const emptyProvider: ProviderConfig = { api_format: undefined, session_id: undefined, user_id: undefined, + use_datastore_upload: undefined, }; export default function AIProviders() { @@ -456,11 +457,28 @@ export default function AIProviders() { placeholder="user@example.com" />

- Optional: Email address for usage tracking. If omitted, costs are attributed to the API key owner. + Prefix added before API key (e.g., "Bearer " for OpenAI, empty for Custom REST)

)} + {/* Datastore upload option */} +
+ +
+ setForm({ ...form, use_datastore_upload: e.target.checked })} + className="w-4 h-4 rounded border-gray-300" + /> + Enable file upload to datastore (for providers that support it) +
+

+ When enabled, files will be uploaded to the provider's datastore before chat messages. This allows the AI to analyze uploaded files. +

+
+ {/* Custom REST specific: model dropdown with custom option */} {form.api_format === CUSTOM_REST_FORMAT && (