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
This commit is contained in:
parent
8a88f046d3
commit
31df73c6d0
@ -16,6 +16,7 @@ export interface ProviderConfig {
|
|||||||
api_format?: string;
|
api_format?: string;
|
||||||
session_id?: string;
|
session_id?: string;
|
||||||
user_id?: string;
|
user_id?: string;
|
||||||
|
use_datastore_upload?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Message {
|
export interface Message {
|
||||||
@ -283,6 +284,9 @@ export const uploadImageAttachmentCmd = (issueId: string, filePath: string) =>
|
|||||||
export const uploadPasteImageCmd = (issueId: string, base64Image: string, mimeType: string) =>
|
export const uploadPasteImageCmd = (issueId: string, base64Image: string, mimeType: string) =>
|
||||||
invoke<ImageAttachment>("upload_paste_image", { issueId, base64Image, mimeType });
|
invoke<ImageAttachment>("upload_paste_image", { issueId, base64Image, mimeType });
|
||||||
|
|
||||||
|
export const uploadFileToDatastoreCmd = (issueId: string, filePath: string, sessionId: string, api_url: string, api_key: string) =>
|
||||||
|
invoke<ImageAttachment>("upload_file_to_datastore", { issueId, filePath, session_id: sessionId, api_url, api_key });
|
||||||
|
|
||||||
export const listImageAttachmentsCmd = (issueId: string) =>
|
export const listImageAttachmentsCmd = (issueId: string) =>
|
||||||
invoke<ImageAttachment[]>("list_image_attachments", { issueId });
|
invoke<ImageAttachment[]>("list_image_attachments", { issueId });
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import {
|
|||||||
detectPiiCmd,
|
detectPiiCmd,
|
||||||
uploadImageAttachmentCmd,
|
uploadImageAttachmentCmd,
|
||||||
uploadPasteImageCmd,
|
uploadPasteImageCmd,
|
||||||
|
uploadFileToDatastoreCmd,
|
||||||
listImageAttachmentsCmd,
|
listImageAttachmentsCmd,
|
||||||
deleteImageAttachmentCmd,
|
deleteImageAttachmentCmd,
|
||||||
type LogFile,
|
type LogFile,
|
||||||
@ -17,6 +18,7 @@ import {
|
|||||||
type ImageAttachment,
|
type ImageAttachment,
|
||||||
} from "@/lib/tauriCommands";
|
} from "@/lib/tauriCommands";
|
||||||
import ImageGallery from "@/components/ImageGallery";
|
import ImageGallery from "@/components/ImageGallery";
|
||||||
|
import { open } from "@tauri-apps/plugin-dialog";
|
||||||
|
|
||||||
export default function LogUpload() {
|
export default function LogUpload() {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
@ -106,23 +108,43 @@ export default function LogUpload() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleImageDrop = useCallback(
|
const handleImageDrop = useCallback(
|
||||||
(e: React.DragEvent) => {
|
async (e: React.DragEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const droppedFiles = Array.from(e.dataTransfer.files);
|
// Use file dialog to get actual paths (drag-and-drop doesn't give paths in Tauri v2)
|
||||||
const imageFiles = droppedFiles.filter((f) => f.type.startsWith("image/"));
|
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 (imageFiles.length > 0) {
|
if (paths.length > 0) {
|
||||||
handleImagesUpload(imageFiles);
|
handleImagesUpload(paths as unknown as File[]);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(`Attachment failed: ${String(err)}`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[id]
|
[id]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleImageFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleImageFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
if (e.target.files) {
|
if (e.target.files && e.target.files.length > 0) {
|
||||||
const selected = Array.from(e.target.files).filter((f) => f.type.startsWith("image/"));
|
try {
|
||||||
if (selected.length > 0) {
|
const selected = await open({
|
||||||
handleImagesUpload(selected);
|
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);
|
setIsUploading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const uploaded = await Promise.all(
|
const uploaded: ImageAttachment[] = await Promise.all(
|
||||||
imageFiles.map(async (file) => {
|
imageFiles.map(async (file) => {
|
||||||
const result = await uploadImageAttachmentCmd(id, file.name);
|
// Extract the path from the file name (which contains the full path from file dialog)
|
||||||
return result;
|
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]);
|
setImages((prev) => [...prev, ...uploaded]);
|
||||||
|
|||||||
@ -64,6 +64,7 @@ const emptyProvider: ProviderConfig = {
|
|||||||
api_format: undefined,
|
api_format: undefined,
|
||||||
session_id: undefined,
|
session_id: undefined,
|
||||||
user_id: undefined,
|
user_id: undefined,
|
||||||
|
use_datastore_upload: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function AIProviders() {
|
export default function AIProviders() {
|
||||||
@ -456,11 +457,28 @@ export default function AIProviders() {
|
|||||||
placeholder="user@example.com"
|
placeholder="user@example.com"
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
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)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Datastore upload option */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Enable File Upload</Label>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={form.use_datastore_upload ?? false}
|
||||||
|
onChange={(e) => setForm({ ...form, use_datastore_upload: e.target.checked })}
|
||||||
|
className="w-4 h-4 rounded border-gray-300"
|
||||||
|
/>
|
||||||
|
<span className="text-sm">Enable file upload to datastore (for providers that support it)</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
When enabled, files will be uploaded to the provider's datastore before chat messages. This allows the AI to analyze uploaded files.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Custom REST specific: model dropdown with custom option */}
|
{/* Custom REST specific: model dropdown with custom option */}
|
||||||
{form.api_format === CUSTOM_REST_FORMAT && (
|
{form.api_format === CUSTOM_REST_FORMAT && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user