sohei1l commited on
Commit
c836ca5
·
1 Parent(s): e791eef

Add user feedback system with tag correction and custom tags

Browse files
Files changed (3) hide show
  1. src/App.css +126 -1
  2. src/App.jsx +138 -8
  3. src/userFeedbackStore.js +213 -0
src/App.css CHANGED
@@ -110,10 +110,24 @@ header p {
110
 
111
  .tags {
112
  display: flex;
113
- flex-wrap: wrap;
114
  gap: 0.5rem;
115
  }
116
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  .tag {
118
  background: linear-gradient(45deg, #646cff, #61dafb);
119
  color: white;
@@ -121,6 +135,117 @@ header p {
121
  border-radius: 20px;
122
  font-size: 0.9rem;
123
  font-weight: 500;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  }
125
 
126
  @media (max-width: 768px) {
 
110
 
111
  .tags {
112
  display: flex;
113
+ flex-direction: column;
114
  gap: 0.5rem;
115
  }
116
 
117
+ .tag-item {
118
+ display: flex;
119
+ align-items: center;
120
+ justify-content: space-between;
121
+ padding: 0.5rem;
122
+ border-radius: 8px;
123
+ background: rgba(100, 108, 255, 0.05);
124
+ transition: all 0.3s ease;
125
+ }
126
+
127
+ .tag-item.has-feedback {
128
+ background: rgba(100, 108, 255, 0.1);
129
+ }
130
+
131
  .tag {
132
  background: linear-gradient(45deg, #646cff, #61dafb);
133
  color: white;
 
135
  border-radius: 20px;
136
  font-size: 0.9rem;
137
  font-weight: 500;
138
+ margin-right: 1rem;
139
+ }
140
+
141
+ .tag.custom {
142
+ background: linear-gradient(45deg, #27ae60, #2ecc71);
143
+ }
144
+
145
+ .tag.negative {
146
+ background: linear-gradient(45deg, #e74c3c, #c0392b);
147
+ opacity: 0.6;
148
+ }
149
+
150
+ .tag-controls {
151
+ display: flex;
152
+ gap: 0.25rem;
153
+ }
154
+
155
+ .feedback-btn {
156
+ background: transparent;
157
+ border: 2px solid #ddd;
158
+ border-radius: 50%;
159
+ width: 32px;
160
+ height: 32px;
161
+ display: flex;
162
+ align-items: center;
163
+ justify-content: center;
164
+ cursor: pointer;
165
+ transition: all 0.3s ease;
166
+ font-size: 0.9rem;
167
+ }
168
+
169
+ .feedback-btn:hover {
170
+ transform: scale(1.1);
171
+ }
172
+
173
+ .feedback-btn.active {
174
+ border-color: #646cff;
175
+ background: #646cff;
176
+ color: white;
177
+ }
178
+
179
+ .add-tag {
180
+ display: flex;
181
+ gap: 0.5rem;
182
+ margin-top: 1rem;
183
+ padding-top: 1rem;
184
+ border-top: 1px solid #eee;
185
+ }
186
+
187
+ .tag-input {
188
+ flex: 1;
189
+ padding: 0.75rem;
190
+ border: 2px solid #ddd;
191
+ border-radius: 8px;
192
+ font-size: 1rem;
193
+ transition: border-color 0.3s ease;
194
+ }
195
+
196
+ .tag-input:focus {
197
+ outline: none;
198
+ border-color: #646cff;
199
+ }
200
+
201
+ .add-tag-btn {
202
+ background: #27ae60;
203
+ color: white;
204
+ border: none;
205
+ padding: 0.75rem 1.5rem;
206
+ border-radius: 8px;
207
+ cursor: pointer;
208
+ font-size: 1rem;
209
+ transition: background 0.3s ease;
210
+ }
211
+
212
+ .add-tag-btn:hover {
213
+ background: #219a52;
214
+ }
215
+
216
+ .frequent-tags {
217
+ margin-top: 1rem;
218
+ padding-top: 1rem;
219
+ border-top: 1px solid #eee;
220
+ }
221
+
222
+ .frequent-tags h4 {
223
+ margin-bottom: 0.5rem;
224
+ color: #666;
225
+ font-size: 0.9rem;
226
+ }
227
+
228
+ .frequent-tag-list {
229
+ display: flex;
230
+ flex-wrap: wrap;
231
+ gap: 0.25rem;
232
+ }
233
+
234
+ .frequent-tag {
235
+ background: #f8f9fa;
236
+ border: 1px solid #ddd;
237
+ color: #666;
238
+ padding: 0.25rem 0.5rem;
239
+ border-radius: 12px;
240
+ font-size: 0.8rem;
241
+ cursor: pointer;
242
+ transition: all 0.3s ease;
243
+ }
244
+
245
+ .frequent-tag:hover {
246
+ background: #646cff;
247
+ color: white;
248
+ border-color: #646cff;
249
  }
250
 
251
  @media (max-width: 768px) {
src/App.jsx CHANGED
@@ -1,5 +1,6 @@
1
- import { useState, useRef } from 'react'
2
  import CLAPProcessor from './clapProcessor'
 
3
  import './App.css'
4
 
5
  function App() {
@@ -8,10 +9,33 @@ function App() {
8
  const [isLoading, setIsLoading] = useState(false)
9
  const [tags, setTags] = useState([])
10
  const [error, setError] = useState(null)
 
 
 
 
11
  const fileInputRef = useRef(null)
12
  const mediaRecorderRef = useRef(null)
13
  const chunksRef = useRef([])
14
  const clapProcessorRef = useRef(null)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
  const handleFileUpload = (event) => {
17
  const file = event.target.files[0]
@@ -76,24 +100,79 @@ function App() {
76
  clapProcessorRef.current = new CLAPProcessor()
77
  }
78
 
 
 
 
79
  const audioBuffer = await clapProcessorRef.current.fileToAudioBuffer(file)
80
  const generatedTags = await clapProcessorRef.current.processAudio(audioBuffer)
81
 
82
- setTags(generatedTags)
 
 
 
 
 
 
 
83
  } catch (err) {
84
  console.error('Error processing audio:', err)
85
  setError('Failed to process audio. Using fallback tags.')
86
  // Fallback tags
87
  setTags([
88
- { label: 'audio', confidence: 0.9 },
89
- { label: 'sound', confidence: 0.8 },
90
- { label: 'recording', confidence: 0.7 }
91
  ])
92
  } finally {
93
  setIsLoading(false)
94
  }
95
  }
96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  return (
98
  <div className="app">
99
  <header>
@@ -156,11 +235,62 @@ function App() {
156
  <h3>Generated Tags</h3>
157
  <div className="tags">
158
  {tags.map((tag, index) => (
159
- <span key={index} className="tag">
160
- {tag.label} ({Math.round(tag.confidence * 100)}%)
161
- </span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  ))}
163
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
  </div>
165
  )}
166
  </main>
 
1
+ import { useState, useRef, useEffect } from 'react'
2
  import CLAPProcessor from './clapProcessor'
3
+ import UserFeedbackStore from './userFeedbackStore'
4
  import './App.css'
5
 
6
  function App() {
 
9
  const [isLoading, setIsLoading] = useState(false)
10
  const [tags, setTags] = useState([])
11
  const [error, setError] = useState(null)
12
+ const [customTags, setCustomTags] = useState([])
13
+ const [newTag, setNewTag] = useState('')
14
+ const [audioHash, setAudioHash] = useState(null)
15
+ const [audioFeatures, setAudioFeatures] = useState(null)
16
  const fileInputRef = useRef(null)
17
  const mediaRecorderRef = useRef(null)
18
  const chunksRef = useRef([])
19
  const clapProcessorRef = useRef(null)
20
+ const feedbackStoreRef = useRef(null)
21
+
22
+ useEffect(() => {
23
+ const initializeStore = async () => {
24
+ feedbackStoreRef.current = new UserFeedbackStore()
25
+ await feedbackStoreRef.current.initialize()
26
+ loadCustomTags()
27
+ }
28
+ initializeStore()
29
+ }, [])
30
+
31
+ const loadCustomTags = async () => {
32
+ try {
33
+ const stored = await feedbackStoreRef.current.getCustomTags()
34
+ setCustomTags(stored.map(item => item.tag))
35
+ } catch (error) {
36
+ console.error('Error loading custom tags:', error)
37
+ }
38
+ }
39
 
40
  const handleFileUpload = (event) => {
41
  const file = event.target.files[0]
 
100
  clapProcessorRef.current = new CLAPProcessor()
101
  }
102
 
103
+ const hash = await feedbackStoreRef.current.hashAudioFile(file)
104
+ setAudioHash(hash)
105
+
106
  const audioBuffer = await clapProcessorRef.current.fileToAudioBuffer(file)
107
  const generatedTags = await clapProcessorRef.current.processAudio(audioBuffer)
108
 
109
+ // Store basic audio info for later use
110
+ setAudioFeatures({
111
+ sampleRate: audioBuffer.sampleRate,
112
+ duration: audioBuffer.duration,
113
+ numberOfChannels: audioBuffer.numberOfChannels
114
+ })
115
+
116
+ setTags(generatedTags.map(tag => ({ ...tag, userFeedback: null })))
117
  } catch (err) {
118
  console.error('Error processing audio:', err)
119
  setError('Failed to process audio. Using fallback tags.')
120
  // Fallback tags
121
  setTags([
122
+ { label: 'audio', confidence: 0.9, userFeedback: null },
123
+ { label: 'sound', confidence: 0.8, userFeedback: null },
124
+ { label: 'recording', confidence: 0.7, userFeedback: null }
125
  ])
126
  } finally {
127
  setIsLoading(false)
128
  }
129
  }
130
 
131
+ const handleTagFeedback = async (tagIndex, feedback) => {
132
+ const updatedTags = [...tags]
133
+ updatedTags[tagIndex].userFeedback = feedback
134
+ setTags(updatedTags)
135
+
136
+ try {
137
+ await feedbackStoreRef.current.saveTagFeedback(
138
+ updatedTags[tagIndex].label,
139
+ feedback,
140
+ audioHash
141
+ )
142
+ } catch (error) {
143
+ console.error('Error saving tag feedback:', error)
144
+ }
145
+ }
146
+
147
+ const handleAddCustomTag = async () => {
148
+ if (!newTag.trim()) return
149
+
150
+ const customTag = {
151
+ label: newTag.trim(),
152
+ confidence: 1.0,
153
+ userFeedback: 'custom',
154
+ isCustom: true
155
+ }
156
+
157
+ setTags(prev => [...prev, customTag])
158
+
159
+ try {
160
+ await feedbackStoreRef.current.saveCustomTag(newTag.trim())
161
+ await feedbackStoreRef.current.saveTagFeedback(newTag.trim(), 'custom', audioHash)
162
+ loadCustomTags()
163
+ } catch (error) {
164
+ console.error('Error saving custom tag:', error)
165
+ }
166
+
167
+ setNewTag('')
168
+ }
169
+
170
+ const handleKeyPress = (e) => {
171
+ if (e.key === 'Enter') {
172
+ handleAddCustomTag()
173
+ }
174
+ }
175
+
176
  return (
177
  <div className="app">
178
  <header>
 
235
  <h3>Generated Tags</h3>
236
  <div className="tags">
237
  {tags.map((tag, index) => (
238
+ <div key={index} className={`tag-item ${tag.userFeedback ? 'has-feedback' : ''}`}>
239
+ <span className={`tag ${tag.isCustom ? 'custom' : ''} ${tag.userFeedback === 'negative' ? 'negative' : ''}`}>
240
+ {tag.label} ({Math.round(tag.confidence * 100)}%)
241
+ </span>
242
+ {!tag.isCustom && (
243
+ <div className="tag-controls">
244
+ <button
245
+ onClick={() => handleTagFeedback(index, 'positive')}
246
+ className={`feedback-btn ${tag.userFeedback === 'positive' ? 'active' : ''}`}
247
+ title="Good tag"
248
+ >
249
+
250
+ </button>
251
+ <button
252
+ onClick={() => handleTagFeedback(index, 'negative')}
253
+ className={`feedback-btn ${tag.userFeedback === 'negative' ? 'active' : ''}`}
254
+ title="Bad tag"
255
+ >
256
+
257
+ </button>
258
+ </div>
259
+ )}
260
+ </div>
261
  ))}
262
  </div>
263
+
264
+ <div className="add-tag">
265
+ <input
266
+ type="text"
267
+ value={newTag}
268
+ onChange={(e) => setNewTag(e.target.value)}
269
+ onKeyPress={handleKeyPress}
270
+ placeholder="Add custom tag..."
271
+ className="tag-input"
272
+ />
273
+ <button onClick={handleAddCustomTag} className="add-tag-btn">
274
+ Add Tag
275
+ </button>
276
+ </div>
277
+
278
+ {customTags.length > 0 && (
279
+ <div className="frequent-tags">
280
+ <h4>Frequent Tags:</h4>
281
+ <div className="frequent-tag-list">
282
+ {customTags.slice(0, 10).map((tag, index) => (
283
+ <button
284
+ key={index}
285
+ onClick={() => setNewTag(tag)}
286
+ className="frequent-tag"
287
+ >
288
+ {tag}
289
+ </button>
290
+ ))}
291
+ </div>
292
+ </div>
293
+ )}
294
  </div>
295
  )}
296
  </main>
src/userFeedbackStore.js ADDED
@@ -0,0 +1,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class UserFeedbackStore {
2
+ constructor() {
3
+ this.dbName = 'ClipTaggerDB';
4
+ this.version = 1;
5
+ this.db = null;
6
+ }
7
+
8
+ async initialize() {
9
+ return new Promise((resolve, reject) => {
10
+ const request = indexedDB.open(this.dbName, this.version);
11
+
12
+ request.onerror = () => reject(request.error);
13
+ request.onsuccess = () => {
14
+ this.db = request.result;
15
+ resolve();
16
+ };
17
+
18
+ request.onupgradeneeded = (event) => {
19
+ const db = event.target.result;
20
+
21
+ // Create object stores
22
+ if (!db.objectStoreNames.contains('audioFeedback')) {
23
+ const audioStore = db.createObjectStore('audioFeedback', {
24
+ keyPath: 'id',
25
+ autoIncrement: true
26
+ });
27
+ audioStore.createIndex('timestamp', 'timestamp', { unique: false });
28
+ }
29
+
30
+ if (!db.objectStoreNames.contains('tagFeedback')) {
31
+ const tagStore = db.createObjectStore('tagFeedback', {
32
+ keyPath: 'id',
33
+ autoIncrement: true
34
+ });
35
+ tagStore.createIndex('tag', 'tag', { unique: false });
36
+ tagStore.createIndex('timestamp', 'timestamp', { unique: false });
37
+ }
38
+
39
+ if (!db.objectStoreNames.contains('customTags')) {
40
+ const customTagStore = db.createObjectStore('customTags', {
41
+ keyPath: 'tag'
42
+ });
43
+ customTagStore.createIndex('usage', 'usage', { unique: false });
44
+ }
45
+ };
46
+ });
47
+ }
48
+
49
+ async saveAudioFeedback(audioHash, originalTags, correctedTags, audioFeatures) {
50
+ if (!this.db) await this.initialize();
51
+
52
+ const transaction = this.db.transaction(['audioFeedback'], 'readwrite');
53
+ const store = transaction.objectStore('audioFeedback');
54
+
55
+ const feedback = {
56
+ audioHash,
57
+ originalTags,
58
+ correctedTags,
59
+ audioFeatures,
60
+ timestamp: Date.now()
61
+ };
62
+
63
+ return new Promise((resolve, reject) => {
64
+ const request = store.add(feedback);
65
+ request.onsuccess = () => resolve(request.result);
66
+ request.onerror = () => reject(request.error);
67
+ });
68
+ }
69
+
70
+ async saveTagFeedback(tag, feedback, audioHash) {
71
+ if (!this.db) await this.initialize();
72
+
73
+ const transaction = this.db.transaction(['tagFeedback'], 'readwrite');
74
+ const store = transaction.objectStore('tagFeedback');
75
+
76
+ const tagFeedback = {
77
+ tag,
78
+ feedback, // 'positive', 'negative', or 'custom'
79
+ audioHash,
80
+ timestamp: Date.now()
81
+ };
82
+
83
+ return new Promise((resolve, reject) => {
84
+ const request = store.add(tagFeedback);
85
+ request.onsuccess = () => resolve(request.result);
86
+ request.onerror = () => reject(request.error);
87
+ });
88
+ }
89
+
90
+ async saveCustomTag(tag) {
91
+ if (!this.db) await this.initialize();
92
+
93
+ const transaction = this.db.transaction(['customTags'], 'readwrite');
94
+ const store = transaction.objectStore('customTags');
95
+
96
+ return new Promise((resolve, reject) => {
97
+ const getRequest = store.get(tag);
98
+ getRequest.onsuccess = () => {
99
+ const existing = getRequest.result;
100
+ const tagData = existing ?
101
+ { ...existing, usage: existing.usage + 1 } :
102
+ { tag, usage: 1, timestamp: Date.now() };
103
+
104
+ const putRequest = store.put(tagData);
105
+ putRequest.onsuccess = () => resolve(putRequest.result);
106
+ putRequest.onerror = () => reject(putRequest.error);
107
+ };
108
+ getRequest.onerror = () => reject(getRequest.error);
109
+ });
110
+ }
111
+
112
+ async getCustomTags(limit = 20) {
113
+ if (!this.db) await this.initialize();
114
+
115
+ const transaction = this.db.transaction(['customTags'], 'readonly');
116
+ const store = transaction.objectStore('customTags');
117
+ const index = store.index('usage');
118
+
119
+ return new Promise((resolve, reject) => {
120
+ const request = index.openCursor(null, 'prev'); // Descending order
121
+ const results = [];
122
+
123
+ request.onsuccess = (event) => {
124
+ const cursor = event.target.result;
125
+ if (cursor && results.length < limit) {
126
+ results.push(cursor.value);
127
+ cursor.continue();
128
+ } else {
129
+ resolve(results);
130
+ }
131
+ };
132
+ request.onerror = () => reject(request.error);
133
+ });
134
+ }
135
+
136
+ async getTagFeedback(tag = null) {
137
+ if (!this.db) await this.initialize();
138
+
139
+ const transaction = this.db.transaction(['tagFeedback'], 'readonly');
140
+ const store = transaction.objectStore('tagFeedback');
141
+
142
+ return new Promise((resolve, reject) => {
143
+ let request;
144
+ if (tag) {
145
+ const index = store.index('tag');
146
+ request = index.getAll(tag);
147
+ } else {
148
+ request = store.getAll();
149
+ }
150
+
151
+ request.onsuccess = () => resolve(request.result);
152
+ request.onerror = () => reject(request.error);
153
+ });
154
+ }
155
+
156
+ async getAudioFeedback(limit = 100) {
157
+ if (!this.db) await this.initialize();
158
+
159
+ const transaction = this.db.transaction(['audioFeedback'], 'readonly');
160
+ const store = transaction.objectStore('audioFeedback');
161
+ const index = store.index('timestamp');
162
+
163
+ return new Promise((resolve, reject) => {
164
+ const request = index.openCursor(null, 'prev'); // Most recent first
165
+ const results = [];
166
+
167
+ request.onsuccess = (event) => {
168
+ const cursor = event.target.result;
169
+ if (cursor && results.length < limit) {
170
+ results.push(cursor.value);
171
+ cursor.continue();
172
+ } else {
173
+ resolve(results);
174
+ }
175
+ };
176
+ request.onerror = () => reject(request.error);
177
+ });
178
+ }
179
+
180
+ // Generate a simple hash for audio content
181
+ async hashAudioFile(file) {
182
+ const arrayBuffer = await file.arrayBuffer();
183
+ const hashBuffer = await crypto.subtle.digest('SHA-256', arrayBuffer);
184
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
185
+ return hashArray.map(b => b.toString(16).padStart(2, '0')).join('').substring(0, 16);
186
+ }
187
+
188
+ async clearAllData() {
189
+ if (!this.db) await this.initialize();
190
+
191
+ const transaction = this.db.transaction(['audioFeedback', 'tagFeedback', 'customTags'], 'readwrite');
192
+
193
+ await Promise.all([
194
+ new Promise((resolve, reject) => {
195
+ const request = transaction.objectStore('audioFeedback').clear();
196
+ request.onsuccess = () => resolve();
197
+ request.onerror = () => reject(request.error);
198
+ }),
199
+ new Promise((resolve, reject) => {
200
+ const request = transaction.objectStore('tagFeedback').clear();
201
+ request.onsuccess = () => resolve();
202
+ request.onerror = () => reject(request.error);
203
+ }),
204
+ new Promise((resolve, reject) => {
205
+ const request = transaction.objectStore('customTags').clear();
206
+ request.onsuccess = () => resolve();
207
+ request.onerror = () => reject(request.error);
208
+ })
209
+ ]);
210
+ }
211
+ }
212
+
213
+ export default UserFeedbackStore;