File size: 8,115 Bytes
85a4687
96812c9
 
 
 
 
 
 
85a4687
 
 
9283c8b
85a4687
 
 
 
 
 
 
 
 
9283c8b
 
85a4687
 
 
9283c8b
 
85a4687
 
 
 
 
9283c8b
96812c9
85a4687
 
9283c8b
 
 
 
 
96812c9
85a4687
 
96812c9
85a4687
 
9283c8b
96812c9
85a4687
ad5cef3
85a4687
 
96812c9
85a4687
 
 
 
e7ba29d
85a4687
e7ba29d
 
 
 
 
 
85a4687
 
 
 
96812c9
ad5cef3
96812c9
9283c8b
96812c9
 
daa5539
85a4687
96812c9
85a4687
 
96812c9
85a4687
96812c9
85a4687
 
9283c8b
96812c9
 
 
 
 
 
85a4687
96812c9
85a4687
 
96812c9
85a4687
 
 
96812c9
 
85a4687
 
673d22a
 
ad5cef3
daa5539
85a4687
 
 
96812c9
 
 
 
673d22a
85a4687
ad5cef3
85a4687
 
 
96812c9
85a4687
 
9283c8b
 
96812c9
 
 
 
85a4687
 
 
96812c9
 
 
 
 
85a4687
 
 
 
 
9283c8b
 
96812c9
 
85a4687
 
 
96812c9
 
 
 
 
85a4687
 
08476ef
85a4687
 
 
 
 
 
 
 
 
 
 
 
9283c8b
 
96812c9
 
85a4687
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9283c8b
85a4687
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96812c9
85a4687
 
96812c9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
// src/App.tsx
import { useState, useRef, useEffect, useCallback } from 'react'
import {
  Section,
  WorkerMessage,
  ZeroShotWorkerInput,
} from '../types'
import { useModel } from '../contexts/ModelContext'

const PLACEHOLDER_REVIEWS: string[] = [
  // battery/charging problems
  'Disappointed with the battery life! The phone barely lasts half a day with regular use. Considering how much I paid for it, I expected better performance in this department.',
  "I bought this phone a week ago, and I'm already frustrated with the battery life. It barely lasts half a day with normal usage. I expected more from a supposedly high-end device",
  "The charging port is so finicky. Sometimes it takes forever to charge, and other times it doesn't even recognize the charger. Frustrating experience!",

  // overheating
  "This phone heats up way too quickly, especially when using demanding apps. It's uncomfortable to hold, and I'm concerned it might damage the internal components over time. Not what I expected",
  "This phone is like holding a hot potato. Video calls turn it into a scalding nightmare. Seriously, can't it keep its cool?",
  "Forget about a heatwave outside; my phone's got its own. It's like a little portable heater. Not what I signed up for.",

  // poor build quality
  'I dropped the phone from a short distance, and the screen cracked easily. Not as durable as I expected from a flagship device.',
  'Took a slight bump in my bag, and the frame got dinged. Are we back in the flip phone era?',
  "So, my phone's been in my pocket with just keys – no ninja moves or anything. Still, it managed to get some scratches. Disappointed with the build quality.",

  // software
  'The software updates are a nightmare. Each update seems to introduce new bugs, and it takes forever for them to be fixed.',
  'Constant crashes and freezes make me want to throw it into a black hole.',
  "Every time I open Instagram, my phone freezes and crashes. It's so frustrating!",

  // other
  "I'm not sure what to make of this phone. It's not bad, but it's not great either. I'm on the fence about it.",
  "I hate the color of this phone. It's so ugly!",
  "This phone sucks! I'm returning it."
].sort(() => Math.random() - 0.5)

const PLACEHOLDER_SECTIONS: string[] = [
  'Battery and charging problems',
  'Overheating',
  'Poor build quality',
  'Software issues',
  'Other'
]

function ZeroShotClassification() {
  const [text, setText] = useState<string>(PLACEHOLDER_REVIEWS.join('\n'))

  const [sections, setSections] = useState<Section[]>(
    PLACEHOLDER_SECTIONS.map((title) => ({ title, items: [] }))
  )

  const { status, setStatus, modelInfo } = useModel()

  // Create a reference to the worker object.
  const worker = useRef<Worker | null>(null)

  // We use the `useEffect` hook to setup the worker as soon as the `App` component is mounted.
  useEffect(() => {
    if (!worker.current) {
      return
      // Create the worker if it does not yet exist.
      // worker.current = new Worker(
      //   new URL('../workers/zero-shot-classification.js', import.meta.url),
      //   {
      //     type: 'module'
      //   }
      // )
    }

    // Create a callback function for messages from the worker thread.
    const onMessageReceived = (e: MessageEvent<WorkerMessage>) => {
      const status = e.data.status
      if (status === 'ready') {
        setStatus('ready')
      } else if (status === 'output') {
        setStatus('output')
        const { sequence, labels, scores } = e.data.output!

        // Threshold for classification
        const label = scores[0] > 0.5 ? labels[0] : 'Other'

        const sectionID =
          sections.map((x) => x.title).indexOf(label) ?? sections.length - 1
        setSections((sections) => {
          const newSections = [...sections]
          newSections[sectionID] = {
            ...newSections[sectionID],
            items: [...newSections[sectionID].items, sequence]
          }
          return newSections
        })
      } else if (status === 'error') {
        setStatus('error')
        console.error(e.data.output)
      }
    }

    // Attach the callback function as an event listener.
    worker.current.addEventListener('message', onMessageReceived)

    // Define a cleanup function for when the component is unmounted.
    return () =>
      worker.current?.removeEventListener('message', onMessageReceived)
  }, [sections])

  const classify = useCallback(() => {
    if (!modelInfo) return

    setStatus('loading')
    const message: ZeroShotWorkerInput = {
      text,
      labels: sections
        .slice(0, sections.length - 1)
        .map((section) => section.title),
      model: modelInfo.name
    }
    worker.current?.postMessage(message)
  }, [text, sections, modelInfo])

  const busy: boolean = status !== 'ready'

  const handleAddCategory = (): void => {
    setSections((sections) => {
      const newSections = [...sections]
      // add at position 2 from the end
      newSections.splice(newSections.length - 1, 0, {
        title: 'New Category',
        items: []
      })
      return newSections
    })
  }

  const handleRemoveCategory = (): void => {
    setSections((sections) => {
      const newSections = [...sections]
      newSections.splice(newSections.length - 2, 1) // Remove second last element
      return newSections
    })
  }

  const handleClear = (): void => {
    setSections((sections) =>
      sections.map((section) => ({
        ...section,
        items: []
      }))
    )
  }

  const handleSectionTitleChange = (index: number, newTitle: string): void => {
    setSections((sections) => {
      const newSections = [...sections]
      newSections[index].title = newTitle
      return newSections
    })
  }

  return (
    <div className="flex flex-col h-screen w-full p-1">
      <textarea
        className="border w-full p-1 h-1/2"
        value={text}
        onChange={(e) => setText(e.target.value)}
      ></textarea>
      <div className="flex flex-col justify-center items-center m-2 gap-1">
        <button
          className="border py-1 px-2 bg-blue-400 rounded text-white text-lg font-medium disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
          disabled={busy}
          onClick={classify}
        >
          {!busy
            ? 'Categorize'
            : status === 'loading'
            ? 'Model loading...'
            : 'Processing'}
        </button>
        <div className="flex gap-1">
          <button
            className="border py-1 px-2 bg-green-400 rounded text-white text-sm font-medium cursor-pointer"
            onClick={handleAddCategory}
          >
            Add category
          </button>
          <button
            className="border py-1 px-2 bg-red-400 rounded text-white text-sm font-medium cursor-pointer"
            disabled={sections.length <= 1}
            onClick={handleRemoveCategory}
          >
            Remove category
          </button>
          <button
            className="border py-1 px-2 bg-orange-400 rounded text-white text-sm font-medium cursor-pointer"
            onClick={handleClear}
          >
            Clear
          </button>
        </div>
      </div>

      <div className="flex justify-between flex-grow overflow-x-auto max-h-[40%]">
        {sections.map((section, index) => (
          <div key={index} className="flex flex-col w-full">
            <input
              disabled={section.title === 'Other'}
              className="w-full border px-1 text-center"
              value={section.title}
              onChange={(e) => handleSectionTitleChange(index, e.target.value)}
            ></input>
            <div className="overflow-y-auto h-full border">
              {section.items.map((item, itemIndex) => (
                <div
                  className="m-2 border bg-red-50 rounded p-1 text-sm"
                  key={itemIndex}
                >
                  {item}
                </div>
              ))}
            </div>
          </div>
        ))}
      </div>
    </div>
  )
}

export default ZeroShotClassification