|
|
|
import { useState, useRef, useEffect, useCallback } from "react"; |
|
import { Section, WorkerMessage, ZeroShotWorkerInput } from "../types"; |
|
|
|
const PLACEHOLDER_REVIEWS: string[] = [ |
|
|
|
"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!", |
|
|
|
|
|
"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.", |
|
|
|
|
|
"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.", |
|
|
|
|
|
"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!", |
|
|
|
|
|
"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] = useState<string>("idle"); |
|
const [progress, setProgress] = useState<number>(0); |
|
|
|
|
|
const worker = useRef<Worker | null>(null); |
|
|
|
|
|
useEffect(() => { |
|
if (!worker.current) { |
|
|
|
worker.current = new Worker( |
|
new URL("../workers/zero-shot.js", import.meta.url), |
|
{ |
|
type: "module", |
|
} |
|
); |
|
} |
|
|
|
|
|
const onMessageReceived = (e: MessageEvent<WorkerMessage>) => { |
|
const status = e.data.status; |
|
if (status === "initiate") { |
|
setStatus("loading"); |
|
} else if (status === "ready") { |
|
setStatus("ready"); |
|
} else if (status === "progress") { |
|
setStatus("progress"); |
|
if (e.data.output.progress && (e.data.output.file as string).startsWith('onnx')) |
|
setProgress(e.data.output.progress) |
|
} else if (status === "output") { |
|
const { sequence, labels, scores } = e.data.output!; |
|
|
|
|
|
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 === "complete") { |
|
setStatus("idle"); |
|
setProgress(100) |
|
} |
|
}; |
|
|
|
|
|
worker.current.addEventListener("message", onMessageReceived); |
|
|
|
|
|
return () => |
|
worker.current?.removeEventListener("message", onMessageReceived); |
|
}, [sections]); |
|
|
|
const classify = useCallback(() => { |
|
setStatus("processing"); |
|
const message: ZeroShotWorkerInput = { |
|
text, |
|
labels: sections |
|
.slice(0, sections.length - 1) |
|
.map((section) => section.title), |
|
}; |
|
worker.current?.postMessage(message); |
|
}, [text, sections]); |
|
|
|
const busy: boolean = status !== "idle"; |
|
|
|
const handleAddCategory = (): void => { |
|
setSections((sections) => { |
|
const newSections = [...sections]; |
|
|
|
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); |
|
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-screen 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> |
|
{ status === "progress" && |
|
<div className="text-sm font-medium"> |
|
{progress}% |
|
</div> |
|
} |
|
<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; |
|
|