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
Files changed (2) hide show
  1. frontend/src/App.jsx +114 -10
  2. 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 [startFolderList, setStartFolderList] = useState("");
 
 
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: selectedFolders.length > 0 ? selectedFolders : null,
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
- Optional selected folders (comma-separated, must match folder names)
183
  <input
184
- value={startFolderList}
185
- onChange={(e) => setStartFolderList(e.target.value)}
186
- placeholder="1girl, solo, blush"
 
 
 
 
 
 
 
 
 
 
 
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", {