Compare commits
3 Commits
859d7a0da8
...
a748df824b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a748df824b | ||
|
|
31df73c6d0 | ||
|
|
8a88f046d3 |
1
src-tauri/Cargo.lock
generated
1
src-tauri/Cargo.lock
generated
@ -4242,6 +4242,7 @@ dependencies = [
|
|||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
"mime",
|
"mime",
|
||||||
|
"mime_guess",
|
||||||
"native-tls",
|
"native-tls",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
|||||||
@ -21,7 +21,7 @@ rusqlite = { version = "0.31", features = ["bundled-sqlcipher-vendored-openssl"]
|
|||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
reqwest = { version = "0.12", features = ["json", "stream"] }
|
reqwest = { version = "0.12", features = ["json", "stream", "multipart"] }
|
||||||
regex = "1"
|
regex = "1"
|
||||||
aho-corasick = "1"
|
aho-corasick = "1"
|
||||||
uuid = { version = "1", features = ["v7"] }
|
uuid = { version = "1", features = ["v7"] }
|
||||||
|
|||||||
@ -8,12 +8,13 @@ use crate::db::models::{AuditEntry, ImageAttachment};
|
|||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
|
||||||
const MAX_IMAGE_FILE_BYTES: u64 = 10 * 1024 * 1024;
|
const MAX_IMAGE_FILE_BYTES: u64 = 10 * 1024 * 1024;
|
||||||
const SUPPORTED_IMAGE_MIME_TYPES: [&str; 5] = [
|
const SUPPORTED_IMAGE_MIME_TYPES: [&str; 6] = [
|
||||||
"image/png",
|
"image/png",
|
||||||
"image/jpeg",
|
"image/jpeg",
|
||||||
"image/gif",
|
"image/gif",
|
||||||
"image/webp",
|
"image/webp",
|
||||||
"image/svg+xml",
|
"image/svg+xml",
|
||||||
|
"image/bmp",
|
||||||
];
|
];
|
||||||
|
|
||||||
fn validate_image_file_path(file_path: &str) -> Result<std::path::PathBuf, String> {
|
fn validate_image_file_path(file_path: &str) -> Result<std::path::PathBuf, String> {
|
||||||
@ -265,6 +266,128 @@ pub async fn delete_image_attachment(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn upload_file_to_datastore(
|
||||||
|
issue_id: String,
|
||||||
|
file_path: String,
|
||||||
|
session_id: String,
|
||||||
|
api_url: String,
|
||||||
|
api_key: String,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
) -> Result<ImageAttachment, String> {
|
||||||
|
use reqwest::Client;
|
||||||
|
|
||||||
|
let canonical_path = std::path::Path::new(&file_path);
|
||||||
|
let canonical = std::fs::canonicalize(canonical_path)
|
||||||
|
.map_err(|_| "Unable to access file")?;
|
||||||
|
|
||||||
|
let file_name = canonical
|
||||||
|
.file_name()
|
||||||
|
.and_then(|n| n.to_str())
|
||||||
|
.unwrap_or("unknown")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let content = std::fs::read(&canonical).map_err(|_| "Failed to read file")?;
|
||||||
|
let file_size = content.len() as i64;
|
||||||
|
|
||||||
|
// Upload hash before consuming content
|
||||||
|
let _upload_hash = format!("{:x}", sha2::Sha256::digest(&content));
|
||||||
|
|
||||||
|
// Clone for upload (reqwest multipart consumes content)
|
||||||
|
let content_clone = content.clone();
|
||||||
|
|
||||||
|
// Build the upload URL
|
||||||
|
let api_url = api_url.trim_end_matches('/');
|
||||||
|
let upload_url = format!("{}/upload/{}", api_url, session_id);
|
||||||
|
|
||||||
|
// Upload file using multipart/form-data
|
||||||
|
let client = Client::builder()
|
||||||
|
.timeout(std::time::Duration::from_secs(60))
|
||||||
|
.build()
|
||||||
|
.map_err(|e| format!("Failed to create HTTP client: {}", e))?;
|
||||||
|
|
||||||
|
let resp = client
|
||||||
|
.post(&upload_url)
|
||||||
|
.header("x-msi-genai-api-key", api_key)
|
||||||
|
.multipart(reqwest::multipart::Form::new().part("file", reqwest::multipart::Part::bytes(content_clone).file_name(file_name.clone())))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to upload file: {}", e))?;
|
||||||
|
|
||||||
|
if !resp.status().is_success() {
|
||||||
|
let status = resp.status();
|
||||||
|
let text = resp.text().await.unwrap_or_else(|_| "unable to read response".to_string());
|
||||||
|
return Err(format!("File upload failed: {} - {}", status, text));
|
||||||
|
}
|
||||||
|
|
||||||
|
let response_json: serde_json::Value = resp.json().await
|
||||||
|
.map_err(|e| format!("Failed to parse upload response: {}", e))?;
|
||||||
|
|
||||||
|
if !response_json["status"].as_bool().unwrap_or(false) {
|
||||||
|
let msg = response_json["msg"].as_str().unwrap_or("Unknown error");
|
||||||
|
return Err(format!("Upload failed: {}", msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create ImageAttachment record
|
||||||
|
let upload_hash = format!("{:x}", sha2::Sha256::digest(&content));
|
||||||
|
let mime_type = infer::get(&content)
|
||||||
|
.map(|m| m.mime_type().to_string())
|
||||||
|
.unwrap_or_else(|| "application/octet-stream".to_string());
|
||||||
|
|
||||||
|
let attachment = ImageAttachment::new(
|
||||||
|
issue_id.clone(),
|
||||||
|
file_name,
|
||||||
|
canonical.to_string_lossy().to_string(),
|
||||||
|
file_size,
|
||||||
|
mime_type,
|
||||||
|
upload_hash,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
let db = state.db.lock().map_err(|e| e.to_string())?;
|
||||||
|
db.execute(
|
||||||
|
"INSERT INTO image_attachments (id, issue_id, file_name, file_path, file_size, mime_type, upload_hash, uploaded_at, pii_warning_acknowledged, is_paste) \
|
||||||
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
|
||||||
|
rusqlite::params![
|
||||||
|
attachment.id,
|
||||||
|
attachment.issue_id,
|
||||||
|
attachment.file_name,
|
||||||
|
attachment.file_path,
|
||||||
|
attachment.file_size,
|
||||||
|
attachment.mime_type,
|
||||||
|
attachment.upload_hash,
|
||||||
|
attachment.uploaded_at,
|
||||||
|
attachment.pii_warning_acknowledged as i32,
|
||||||
|
attachment.is_paste as i32,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.map_err(|_| "Failed to store file metadata".to_string())?;
|
||||||
|
|
||||||
|
let entry = AuditEntry::new(
|
||||||
|
"upload_file_to_datastore".to_string(),
|
||||||
|
"image_attachment".to_string(),
|
||||||
|
attachment.id.clone(),
|
||||||
|
serde_json::json!({
|
||||||
|
"issue_id": issue_id,
|
||||||
|
"file_name": attachment.file_name,
|
||||||
|
"session_id": session_id,
|
||||||
|
})
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
if let Err(err) = write_audit_event(
|
||||||
|
&db,
|
||||||
|
&entry.action,
|
||||||
|
&entry.entity_type,
|
||||||
|
&entry.entity_id,
|
||||||
|
&entry.details,
|
||||||
|
) {
|
||||||
|
tracing::warn!(error = %err, "failed to write upload_file_to_datastore audit entry");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(attachment)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@ -276,7 +399,13 @@ mod tests {
|
|||||||
assert!(is_supported_image_format("image/gif"));
|
assert!(is_supported_image_format("image/gif"));
|
||||||
assert!(is_supported_image_format("image/webp"));
|
assert!(is_supported_image_format("image/webp"));
|
||||||
assert!(is_supported_image_format("image/svg+xml"));
|
assert!(is_supported_image_format("image/svg+xml"));
|
||||||
assert!(!is_supported_image_format("image/bmp"));
|
assert!(is_supported_image_format("image/bmp"));
|
||||||
assert!(!is_supported_image_format("text/plain"));
|
assert!(!is_supported_image_format("text/plain"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_bmp_support() {
|
||||||
|
// BMP support added
|
||||||
|
assert!(is_supported_image_format("image/bmp"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -158,8 +158,8 @@ pub async fn save_ai_provider(
|
|||||||
db.execute(
|
db.execute(
|
||||||
"INSERT OR REPLACE INTO ai_providers
|
"INSERT OR REPLACE INTO ai_providers
|
||||||
(id, name, provider_type, api_url, encrypted_api_key, model, max_tokens, temperature,
|
(id, name, provider_type, api_url, encrypted_api_key, model, max_tokens, temperature,
|
||||||
custom_endpoint_path, custom_auth_header, custom_auth_prefix, api_format, user_id, updated_at)
|
custom_endpoint_path, custom_auth_header, custom_auth_prefix, api_format, user_id, use_datastore_upload, updated_at)
|
||||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, datetime('now'))",
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, datetime('now'))",
|
||||||
rusqlite::params![
|
rusqlite::params![
|
||||||
uuid::Uuid::now_v7().to_string(),
|
uuid::Uuid::now_v7().to_string(),
|
||||||
provider.name,
|
provider.name,
|
||||||
@ -174,6 +174,7 @@ pub async fn save_ai_provider(
|
|||||||
provider.custom_auth_prefix,
|
provider.custom_auth_prefix,
|
||||||
provider.api_format,
|
provider.api_format,
|
||||||
provider.user_id,
|
provider.user_id,
|
||||||
|
provider.use_datastore_upload,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
.map_err(|e| format!("Failed to save AI provider: {e}"))?;
|
.map_err(|e| format!("Failed to save AI provider: {e}"))?;
|
||||||
@ -190,10 +191,11 @@ pub async fn load_ai_providers(
|
|||||||
|
|
||||||
let mut stmt = db
|
let mut stmt = db
|
||||||
.prepare(
|
.prepare(
|
||||||
"SELECT name, provider_type, api_url, encrypted_api_key, model, max_tokens, temperature,
|
"SELECT name, provider_type, api_url, encrypted_api_key, model, max_tokens, temperature,
|
||||||
custom_endpoint_path, custom_auth_header, custom_auth_prefix, api_format, user_id
|
custom_endpoint_path, custom_auth_header, custom_auth_prefix, api_format, user_id,
|
||||||
FROM ai_providers
|
use_datastore_upload
|
||||||
ORDER BY name",
|
FROM ai_providers
|
||||||
|
ORDER BY name",
|
||||||
)
|
)
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
@ -212,29 +214,31 @@ pub async fn load_ai_providers(
|
|||||||
row.get::<_, Option<String>>(7)?, // custom_endpoint_path
|
row.get::<_, Option<String>>(7)?, // custom_endpoint_path
|
||||||
row.get::<_, Option<String>>(8)?, // custom_auth_header
|
row.get::<_, Option<String>>(8)?, // custom_auth_header
|
||||||
row.get::<_, Option<String>>(9)?, // custom_auth_prefix
|
row.get::<_, Option<String>>(9)?, // custom_auth_prefix
|
||||||
row.get::<_, Option<String>>(10)?, // api_format
|
row.get::<_, Option<String>>(10)?, // api_format
|
||||||
row.get::<_, Option<String>>(11)?, // user_id
|
row.get::<_, Option<String>>(11)?, // user_id
|
||||||
))
|
row.get::<_, Option<bool>>(12)?, // use_datastore_upload
|
||||||
})
|
))
|
||||||
.map_err(|e| e.to_string())?
|
})
|
||||||
.filter_map(|r| r.ok())
|
.map_err(|e| e.to_string())?
|
||||||
.filter_map(
|
.filter_map(|r| r.ok())
|
||||||
|(
|
.filter_map(
|
||||||
name,
|
|(
|
||||||
provider_type,
|
name,
|
||||||
api_url,
|
provider_type,
|
||||||
encrypted_key,
|
api_url,
|
||||||
model,
|
encrypted_key,
|
||||||
max_tokens,
|
model,
|
||||||
temperature,
|
max_tokens,
|
||||||
custom_endpoint_path,
|
temperature,
|
||||||
custom_auth_header,
|
custom_endpoint_path,
|
||||||
custom_auth_prefix,
|
custom_auth_header,
|
||||||
api_format,
|
custom_auth_prefix,
|
||||||
user_id,
|
api_format,
|
||||||
)| {
|
user_id,
|
||||||
// Decrypt the API key
|
_use_datastore_upload,
|
||||||
let api_key = crate::integrations::auth::decrypt_token(&encrypted_key).ok()?;
|
)| {
|
||||||
|
// Decrypt the API key
|
||||||
|
let api_key = crate::integrations::auth::decrypt_token(&encrypted_key).ok()?;
|
||||||
|
|
||||||
Some(ProviderConfig {
|
Some(ProviderConfig {
|
||||||
name,
|
name,
|
||||||
@ -250,6 +254,7 @@ pub async fn load_ai_providers(
|
|||||||
api_format,
|
api_format,
|
||||||
session_id: None, // Session IDs are not persisted
|
session_id: None, // Session IDs are not persisted
|
||||||
user_id,
|
user_id,
|
||||||
|
use_datastore_upload: None,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@ -73,11 +73,12 @@ pub fn run() {
|
|||||||
commands::analysis::upload_log_file,
|
commands::analysis::upload_log_file,
|
||||||
commands::analysis::detect_pii,
|
commands::analysis::detect_pii,
|
||||||
commands::analysis::apply_redactions,
|
commands::analysis::apply_redactions,
|
||||||
commands::image::upload_image_attachment,
|
commands::image::upload_image_attachment,
|
||||||
commands::image::list_image_attachments,
|
commands::image::list_image_attachments,
|
||||||
commands::image::delete_image_attachment,
|
commands::image::delete_image_attachment,
|
||||||
commands::image::upload_paste_image,
|
commands::image::upload_paste_image,
|
||||||
// AI
|
commands::image::upload_file_to_datastore,
|
||||||
|
// AI
|
||||||
commands::ai::analyze_logs,
|
commands::ai::analyze_logs,
|
||||||
commands::ai::chat_message,
|
commands::ai::chat_message,
|
||||||
commands::ai::test_provider_connection,
|
commands::ai::test_provider_connection,
|
||||||
|
|||||||
@ -39,6 +39,9 @@ pub struct ProviderConfig {
|
|||||||
/// Optional: User ID for custom REST API cost tracking (CORE ID email)
|
/// Optional: User ID for custom REST API cost tracking (CORE ID email)
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub user_id: Option<String>,
|
pub user_id: Option<String>,
|
||||||
|
/// Optional: Enable file upload to datastore (for providers that support it)
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub use_datastore_upload: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|||||||
@ -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({
|
||||||
if (imageFiles.length > 0) {
|
multiple: true,
|
||||||
handleImagesUpload(imageFiles);
|
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]
|
[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