Dinamush commited on
Commit ·
ebcaa17
1
Parent(s): fa3f072
Add tag search functionality to the App component. Implemented mock API for fetching tags based on user input, allowing for tag selection and validation against existing tags. Updated state management to handle selected tags and integrated UI elements for tag input and display.
Browse files- frontend/src/App.jsx +114 -10
- frontend/src/api.js +26 -0
frontend/src/App.jsx
CHANGED
|
@@ -18,7 +18,9 @@ function App() {
|
|
| 18 |
const [error, setError] = useState("");
|
| 19 |
const [migrateMode, setMigrateMode] = useState("copy");
|
| 20 |
const [selectedIds, setSelectedIds] = useState([]);
|
| 21 |
-
const [
|
|
|
|
|
|
|
| 22 |
|
| 23 |
async function refreshItems(currentRunId) {
|
| 24 |
if (!currentRunId) return;
|
|
@@ -40,6 +42,25 @@ function App() {
|
|
| 40 |
.catch((err) => setError(err.message));
|
| 41 |
}, []);
|
| 42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
const stats = useMemo(() => {
|
| 44 |
return {
|
| 45 |
total: items.length,
|
|
@@ -68,13 +89,9 @@ function App() {
|
|
| 68 |
setLoading(true);
|
| 69 |
setError("");
|
| 70 |
try {
|
| 71 |
-
const selectedFolders = startFolderList
|
| 72 |
-
.split(",")
|
| 73 |
-
.map((x) => x.trim())
|
| 74 |
-
.filter(Boolean);
|
| 75 |
const result = await api.startRun({
|
| 76 |
...settings,
|
| 77 |
-
selected_folders:
|
| 78 |
});
|
| 79 |
setOfflineMode(api.isOfflineMode());
|
| 80 |
setRunId(result.run_id);
|
|
@@ -117,6 +134,49 @@ function App() {
|
|
| 117 |
}
|
| 118 |
}
|
| 119 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
return (
|
| 121 |
<div className="container">
|
| 122 |
<h1>Image Classifier Workflow</h1>
|
|
@@ -179,13 +239,57 @@ function App() {
|
|
| 179 |
<section className="card">
|
| 180 |
<h2>Run Classification</h2>
|
| 181 |
<label>
|
| 182 |
-
|
| 183 |
<input
|
| 184 |
-
value={
|
| 185 |
-
onChange={(e) =>
|
| 186 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
/>
|
| 188 |
</label>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
<button disabled={loading} onClick={handleStartRun}>
|
| 190 |
Start Run
|
| 191 |
</button>
|
|
|
|
| 18 |
const [error, setError] = useState("");
|
| 19 |
const [migrateMode, setMigrateMode] = useState("copy");
|
| 20 |
const [selectedIds, setSelectedIds] = useState([]);
|
| 21 |
+
const [tagQuery, setTagQuery] = useState("");
|
| 22 |
+
const [tagOptions, setTagOptions] = useState([]);
|
| 23 |
+
const [selectedTags, setSelectedTags] = useState([]);
|
| 24 |
|
| 25 |
async function refreshItems(currentRunId) {
|
| 26 |
if (!currentRunId) return;
|
|
|
|
| 42 |
.catch((err) => setError(err.message));
|
| 43 |
}, []);
|
| 44 |
|
| 45 |
+
useEffect(() => {
|
| 46 |
+
let cancelled = false;
|
| 47 |
+
const timer = setTimeout(() => {
|
| 48 |
+
api.getTags(tagQuery, 50)
|
| 49 |
+
.then((data) => {
|
| 50 |
+
if (cancelled) return;
|
| 51 |
+
setTagOptions(data.items || []);
|
| 52 |
+
})
|
| 53 |
+
.catch(() => {
|
| 54 |
+
if (cancelled) return;
|
| 55 |
+
setTagOptions([]);
|
| 56 |
+
});
|
| 57 |
+
}, 150);
|
| 58 |
+
return () => {
|
| 59 |
+
cancelled = true;
|
| 60 |
+
clearTimeout(timer);
|
| 61 |
+
};
|
| 62 |
+
}, [tagQuery]);
|
| 63 |
+
|
| 64 |
const stats = useMemo(() => {
|
| 65 |
return {
|
| 66 |
total: items.length,
|
|
|
|
| 89 |
setLoading(true);
|
| 90 |
setError("");
|
| 91 |
try {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
const result = await api.startRun({
|
| 93 |
...settings,
|
| 94 |
+
selected_folders: selectedTags.length > 0 ? selectedTags : null,
|
| 95 |
});
|
| 96 |
setOfflineMode(api.isOfflineMode());
|
| 97 |
setRunId(result.run_id);
|
|
|
|
| 134 |
}
|
| 135 |
}
|
| 136 |
|
| 137 |
+
function addSelectedTag(value) {
|
| 138 |
+
if (!value) return;
|
| 139 |
+
if (selectedTags.includes(value)) return;
|
| 140 |
+
setSelectedTags([...selectedTags, value]);
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
async function addValidatedTag(rawValue) {
|
| 144 |
+
const value = rawValue.trim();
|
| 145 |
+
if (!value) return;
|
| 146 |
+
if (selectedTags.includes(value)) return;
|
| 147 |
+
|
| 148 |
+
// Fast path when current dropdown options already include the tag.
|
| 149 |
+
if (tagOptions.includes(value)) {
|
| 150 |
+
addSelectedTag(value);
|
| 151 |
+
return;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
// Validate against backend tag index to avoid accidental typo tags.
|
| 155 |
+
const result = await api.getTags(value, 200);
|
| 156 |
+
if ((result.items || []).includes(value)) {
|
| 157 |
+
addSelectedTag(value);
|
| 158 |
+
return;
|
| 159 |
+
}
|
| 160 |
+
throw new Error(`Tag not found in tags.csv: ${value}`);
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
async function addTagsFromInput(raw) {
|
| 164 |
+
const parts = raw
|
| 165 |
+
.split(",")
|
| 166 |
+
.map((x) => x.trim())
|
| 167 |
+
.filter(Boolean);
|
| 168 |
+
if (parts.length === 0) return;
|
| 169 |
+
|
| 170 |
+
for (const part of parts) {
|
| 171 |
+
await addValidatedTag(part);
|
| 172 |
+
}
|
| 173 |
+
setTagQuery("");
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
function removeSelectedTag(value) {
|
| 177 |
+
setSelectedTags(selectedTags.filter((t) => t !== value));
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
return (
|
| 181 |
<div className="container">
|
| 182 |
<h1>Image Classifier Workflow</h1>
|
|
|
|
| 239 |
<section className="card">
|
| 240 |
<h2>Run Classification</h2>
|
| 241 |
<label>
|
| 242 |
+
Tag match search (from tags.csv)
|
| 243 |
<input
|
| 244 |
+
value={tagQuery}
|
| 245 |
+
onChange={(e) => setTagQuery(e.target.value)}
|
| 246 |
+
onKeyDown={async (e) => {
|
| 247 |
+
if (e.key === "Enter") {
|
| 248 |
+
e.preventDefault();
|
| 249 |
+
setError("");
|
| 250 |
+
try {
|
| 251 |
+
await addTagsFromInput(tagQuery);
|
| 252 |
+
} catch (err) {
|
| 253 |
+
setError(err.message);
|
| 254 |
+
}
|
| 255 |
+
}
|
| 256 |
+
}}
|
| 257 |
+
placeholder="Type to search tags..."
|
| 258 |
/>
|
| 259 |
</label>
|
| 260 |
+
<div className="actions">
|
| 261 |
+
<select defaultValue="" onChange={(e) => addSelectedTag(e.target.value)}>
|
| 262 |
+
<option value="" disabled>
|
| 263 |
+
Select matching tag
|
| 264 |
+
</option>
|
| 265 |
+
{tagOptions.map((tag) => (
|
| 266 |
+
<option key={tag} value={tag}>
|
| 267 |
+
{tag}
|
| 268 |
+
</option>
|
| 269 |
+
))}
|
| 270 |
+
</select>
|
| 271 |
+
<button
|
| 272 |
+
onClick={async () => {
|
| 273 |
+
setError("");
|
| 274 |
+
try {
|
| 275 |
+
await addTagsFromInput(tagQuery);
|
| 276 |
+
} catch (err) {
|
| 277 |
+
setError(err.message);
|
| 278 |
+
}
|
| 279 |
+
}}
|
| 280 |
+
>
|
| 281 |
+
Add typed tag(s)
|
| 282 |
+
</button>
|
| 283 |
+
</div>
|
| 284 |
+
<div className="stats">
|
| 285 |
+
<span>Selected tags:</span>
|
| 286 |
+
{selectedTags.length === 0 && <span>none</span>}
|
| 287 |
+
{selectedTags.map((tag) => (
|
| 288 |
+
<span key={tag}>
|
| 289 |
+
{tag} <button onClick={() => removeSelectedTag(tag)}>x</button>
|
| 290 |
+
</span>
|
| 291 |
+
))}
|
| 292 |
+
</div>
|
| 293 |
<button disabled={loading} onClick={handleStartRun}>
|
| 294 |
Start Run
|
| 295 |
</button>
|
frontend/src/api.js
CHANGED
|
@@ -7,6 +7,20 @@ const defaultSettings = {
|
|
| 7 |
confidence_threshold: 0.6,
|
| 8 |
default_migrate_mode: "copy",
|
| 9 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
const mockState = loadMockState();
|
| 12 |
let backendAvailable = null;
|
|
@@ -108,6 +122,14 @@ function mockRequest(path, options = {}) {
|
|
| 108 |
if (path === "/settings" && method === "GET") {
|
| 109 |
return Promise.resolve(mockState.settings);
|
| 110 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
if (path === "/settings" && method === "PUT") {
|
| 112 |
mockState.settings = { ...defaultSettings, ...body };
|
| 113 |
persistMockState();
|
|
@@ -226,6 +248,10 @@ function mockRequest(path, options = {}) {
|
|
| 226 |
|
| 227 |
export const api = {
|
| 228 |
isOfflineMode: () => backendAvailable === false,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 229 |
getSettings: () => request("/settings"),
|
| 230 |
saveSettings: (payload) =>
|
| 231 |
request("/settings", {
|
|
|
|
| 7 |
confidence_threshold: 0.6,
|
| 8 |
default_migrate_mode: "copy",
|
| 9 |
};
|
| 10 |
+
const mockTags = [
|
| 11 |
+
"1girl",
|
| 12 |
+
"solo",
|
| 13 |
+
"blush",
|
| 14 |
+
"black_hair",
|
| 15 |
+
"brown_hair",
|
| 16 |
+
"blue_eyes",
|
| 17 |
+
"short_hair",
|
| 18 |
+
"long_hair",
|
| 19 |
+
"twintails",
|
| 20 |
+
"smile",
|
| 21 |
+
"open_mouth",
|
| 22 |
+
"looking_at_viewer",
|
| 23 |
+
];
|
| 24 |
|
| 25 |
const mockState = loadMockState();
|
| 26 |
let backendAvailable = null;
|
|
|
|
| 122 |
if (path === "/settings" && method === "GET") {
|
| 123 |
return Promise.resolve(mockState.settings);
|
| 124 |
}
|
| 125 |
+
if (path.startsWith("/tags") && method === "GET") {
|
| 126 |
+
const queryString = path.includes("?") ? path.split("?")[1] : "";
|
| 127 |
+
const params = new URLSearchParams(queryString);
|
| 128 |
+
const query = (params.get("query") || "").toLowerCase();
|
| 129 |
+
const limit = Number(params.get("limit") || 50);
|
| 130 |
+
const items = mockTags.filter((t) => t.toLowerCase().includes(query)).slice(0, limit);
|
| 131 |
+
return Promise.resolve({ items, count: items.length });
|
| 132 |
+
}
|
| 133 |
if (path === "/settings" && method === "PUT") {
|
| 134 |
mockState.settings = { ...defaultSettings, ...body };
|
| 135 |
persistMockState();
|
|
|
|
| 248 |
|
| 249 |
export const api = {
|
| 250 |
isOfflineMode: () => backendAvailable === false,
|
| 251 |
+
getTags: (query = "", limit = 50) => {
|
| 252 |
+
const params = new URLSearchParams({ query, limit: String(limit) });
|
| 253 |
+
return request(`/tags?${params.toString()}`);
|
| 254 |
+
},
|
| 255 |
getSettings: () => request("/settings"),
|
| 256 |
saveSettings: (payload) =>
|
| 257 |
request("/settings", {
|