Studio: Add dataset upload dropzone and update preserve think copy (#5253)
* fix: improve training dataset upload affordance * chore: update preserve think * fix: guard dataset dropzone drag target
This commit is contained in:
parent
7d227ed708
commit
874e4605ef
3 changed files with 102 additions and 18 deletions
|
|
@ -485,7 +485,7 @@ const PreserveThinkingToggle: FC = () => {
|
|||
: "bg-muted text-muted-foreground hover:bg-muted-foreground/15",
|
||||
)}
|
||||
aria-label={
|
||||
preserveThinking ? "Disable preserve thinking" : "Enable preserve thinking"
|
||||
preserveThinking ? "Disable preserve think" : "Enable preserve think"
|
||||
}
|
||||
>
|
||||
{preserveThinking && !disabled ? (
|
||||
|
|
@ -493,7 +493,7 @@ const PreserveThinkingToggle: FC = () => {
|
|||
) : (
|
||||
<LightbulbOffIcon className="size-3.5" />
|
||||
)}
|
||||
<span>Preserve Thinking</span>
|
||||
<span>Preserve Think</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -662,7 +662,7 @@ export function SharedComposer({
|
|||
: "bg-muted text-muted-foreground hover:bg-muted-foreground/15",
|
||||
)}
|
||||
aria-label={
|
||||
preserveThinking ? "Disable preserve thinking" : "Enable preserve thinking"
|
||||
preserveThinking ? "Disable preserve think" : "Enable preserve think"
|
||||
}
|
||||
>
|
||||
{preserveThinking && modelLoaded ? (
|
||||
|
|
@ -670,7 +670,7 @@ export function SharedComposer({
|
|||
) : (
|
||||
<LightbulbOffIcon className="size-3.5" />
|
||||
)}
|
||||
<span>Preserve Thinking</span>
|
||||
<span>Preserve Think</span>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -58,17 +58,42 @@ import {
|
|||
ViewIcon,
|
||||
} from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { type ChangeEvent, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
type ChangeEvent,
|
||||
type DragEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { toast } from "sonner";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
import { DocumentUploadRedirectDialog } from "./document-upload-redirect-dialog";
|
||||
|
||||
const TRAINING_UPLOAD_EXTENSIONS = [
|
||||
".csv",
|
||||
".jsonl",
|
||||
".json",
|
||||
".parquet",
|
||||
".pdf",
|
||||
".docx",
|
||||
".txt",
|
||||
] as const;
|
||||
const TRAINING_UPLOAD_EXTENSION_SET = new Set<string>(TRAINING_UPLOAD_EXTENSIONS);
|
||||
const TRAINING_UPLOAD_ACCEPT = TRAINING_UPLOAD_EXTENSIONS.join(",");
|
||||
const TRAINING_UPLOAD_LABEL = "CSV, JSONL, JSON, Parquet, PDF, DOCX, TXT";
|
||||
const DOCUMENT_REDIRECT_EXTENSIONS = new Set([".pdf", ".docx", ".txt"]);
|
||||
|
||||
const SEARCH_INPUT_REASONS = new Set(["input-change", "input-paste", "input-clear"]);
|
||||
const OPEN_LEARNING_RECIPES_ON_ARRIVAL_KEY =
|
||||
"data-recipes:open-learning-recipes";
|
||||
|
||||
function getFileExtension(fileName: string) {
|
||||
const extensionStart = fileName.lastIndexOf(".");
|
||||
return extensionStart >= 0 ? fileName.slice(extensionStart).toLowerCase() : "";
|
||||
}
|
||||
|
||||
function isLikelyLocalDatasetRef(value: string) {
|
||||
return (
|
||||
value.startsWith("/") ||
|
||||
|
|
@ -354,6 +379,7 @@ export function DatasetSection() {
|
|||
);
|
||||
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [isDatasetDragOver, setIsDatasetDragOver] = useState(false);
|
||||
const [documentRedirectOpen, setDocumentRedirectOpen] = useState(false);
|
||||
const [redirectFileName, setRedirectFileName] = useState<string | null>(null);
|
||||
|
||||
|
|
@ -380,12 +406,15 @@ export function DatasetSection() {
|
|||
}
|
||||
};
|
||||
|
||||
const handleDatasetFileChange = async (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
event.target.value = "";
|
||||
if (!file) return;
|
||||
const handleDatasetFile = async (file: File) => {
|
||||
const extension = getFileExtension(file.name);
|
||||
if (!TRAINING_UPLOAD_EXTENSION_SET.has(extension)) {
|
||||
toast.error("Unsupported file type", {
|
||||
description: `Upload one ${TRAINING_UPLOAD_LABEL} file.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const extension = file.name.slice(file.name.lastIndexOf(".")).toLowerCase();
|
||||
if (DOCUMENT_REDIRECT_EXTENSIONS.has(extension)) {
|
||||
setRedirectFileName(file.name);
|
||||
setDocumentRedirectOpen(true);
|
||||
|
|
@ -395,6 +424,44 @@ export function DatasetSection() {
|
|||
await handleFileUpload(file, selectLocalDataset, "Dataset uploaded");
|
||||
};
|
||||
|
||||
const handleDatasetFileChange = async (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
event.target.value = "";
|
||||
if (!file) return;
|
||||
|
||||
await handleDatasetFile(file);
|
||||
};
|
||||
|
||||
const handleDatasetDrop = (event: DragEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
setIsDatasetDragOver(false);
|
||||
if (isUploading) return;
|
||||
|
||||
const files = Array.from(event.dataTransfer.files);
|
||||
if (files.length === 0) return;
|
||||
|
||||
if (files.length > 1) {
|
||||
toast.error("Upload one file at a time", {
|
||||
description: "Training dataset upload accepts a single file.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
void handleDatasetFile(files[0]);
|
||||
};
|
||||
|
||||
const handleDatasetDragOver = (event: DragEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
if (isUploading) return;
|
||||
|
||||
event.dataTransfer.dropEffect = "copy";
|
||||
setIsDatasetDragOver(true);
|
||||
};
|
||||
|
||||
const handleDatasetDragLeave = () => {
|
||||
setIsDatasetDragOver(false);
|
||||
};
|
||||
|
||||
const handleEvalFileChange = async (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
event.target.value = "";
|
||||
|
|
@ -924,7 +991,7 @@ export function DatasetSection() {
|
|||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
||||
<div className="flex flex-col gap-4 pt-1">
|
||||
<div className="flex flex-col gap-3">
|
||||
{selectedDatasetName ? (
|
||||
<div className="flex items-center gap-3 rounded-lg border bg-muted/40 px-3.5 py-3">
|
||||
<div className="rounded-md bg-indigo-500/10 p-1.5">
|
||||
|
|
@ -971,15 +1038,32 @@ export function DatasetSection() {
|
|||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-3 rounded-lg border border-dashed bg-muted/20 px-3.5 py-3">
|
||||
<button
|
||||
type="button"
|
||||
className={`flex w-full cursor-pointer items-center gap-3 rounded-lg border border-dashed px-3.5 py-3 text-left transition-colors ${
|
||||
isDatasetDragOver
|
||||
? "border-indigo-500/70 bg-indigo-500/10"
|
||||
: "border-border bg-muted/20 hover:border-indigo-500/50 hover:bg-indigo-500/5"
|
||||
}`}
|
||||
disabled={isUploading}
|
||||
onClick={handleUploadButtonClick}
|
||||
onDrop={handleDatasetDrop}
|
||||
onDragOver={handleDatasetDragOver}
|
||||
onDragLeave={handleDatasetDragLeave}
|
||||
>
|
||||
<HugeiconsIcon
|
||||
icon={Database02Icon}
|
||||
className="size-4 text-muted-foreground/40"
|
||||
icon={CloudUploadIcon}
|
||||
className="pointer-events-none size-4 shrink-0 text-indigo-500"
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
No dataset selected
|
||||
<span className="pointer-events-none min-w-0">
|
||||
<span className="block text-xs font-medium text-foreground">
|
||||
Drop 1 file here or click to upload
|
||||
</span>
|
||||
<span className="mt-0.5 block truncate text-[10px] text-muted-foreground">
|
||||
{TRAINING_UPLOAD_LABEL}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
|
|
@ -1012,7 +1096,7 @@ export function DatasetSection() {
|
|||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".json,.jsonl,.csv,.parquet,.pdf,.docx,.txt"
|
||||
accept={TRAINING_UPLOAD_ACCEPT}
|
||||
className="hidden"
|
||||
onChange={(event) => {
|
||||
void handleDatasetFileChange(event);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue