complete
Browse files- app/(index)/SentimentList.tsx +4 -6
- app/(index)/SentimentWidget.tsx +11 -20
- app/(index)/TextWidget.tsx +1 -1
- app/(index)/page.tsx +13 -4
- app/api/classify/route.ts +2 -0
- components/title-bar/TitleBar.tsx +4 -1
- next.config.ts +16 -0
- package.json +1 -1
- pipeline.ts +3 -0
- util/classify.ts +15 -0
- yarn.lock +12 -18
app/(index)/SentimentList.tsx
CHANGED
|
@@ -1,18 +1,16 @@
|
|
| 1 |
'use client'
|
| 2 |
import { SentimentDisplay } from "@/components/sentiment-display/SentimentDisplay";
|
| 3 |
-
import {
|
| 4 |
|
| 5 |
export interface ISentimentList {
|
| 6 |
-
|
| 7 |
}
|
| 8 |
|
| 9 |
-
export function SentimentList({
|
| 10 |
-
const out = use(dataPromise);
|
| 11 |
-
|
| 12 |
return (
|
| 13 |
<div className="flex flex-col gap-8 p-12 overflow-y-auto">
|
| 14 |
{Array.isArray(out) ?
|
| 15 |
-
out.map(({ label, score }
|
| 16 |
<SentimentDisplay key={label} label={label} value={score} />
|
| 17 |
) :
|
| 18 |
<p>Begin to write to see results!</p>
|
|
|
|
| 1 |
'use client'
|
| 2 |
import { SentimentDisplay } from "@/components/sentiment-display/SentimentDisplay";
|
| 3 |
+
import { ClassifyOutputElement } from "@/util/classify";
|
| 4 |
|
| 5 |
export interface ISentimentList {
|
| 6 |
+
out?: ClassifyOutputElement[];
|
| 7 |
}
|
| 8 |
|
| 9 |
+
export function SentimentList({ out }: ISentimentList) {
|
|
|
|
|
|
|
| 10 |
return (
|
| 11 |
<div className="flex flex-col gap-8 p-12 overflow-y-auto">
|
| 12 |
{Array.isArray(out) ?
|
| 13 |
+
out.map(({ label, score }) =>
|
| 14 |
<SentimentDisplay key={label} label={label} value={score} />
|
| 15 |
) :
|
| 16 |
<p>Begin to write to see results!</p>
|
app/(index)/SentimentWidget.tsx
CHANGED
|
@@ -1,30 +1,22 @@
|
|
| 1 |
'use client'
|
| 2 |
import { TitleBar } from "@/components/title-bar/TitleBar";
|
| 3 |
import { useTextStore } from "./TextStore";
|
| 4 |
-
import { Suspense } from "react";
|
| 5 |
import { SentimentList } from "./SentimentList";
|
| 6 |
import { Fallback } from "@/components/fallback/Fallback";
|
| 7 |
import Link from "next/link";
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
const raw = await fetch('/api/classify', {
|
| 11 |
-
method: 'POST',
|
| 12 |
-
headers: {
|
| 13 |
-
'Accept': 'application/json',
|
| 14 |
-
'Content-Type': 'application/json'
|
| 15 |
-
},
|
| 16 |
-
body: JSON.stringify({
|
| 17 |
-
text,
|
| 18 |
-
})
|
| 19 |
-
});
|
| 20 |
-
|
| 21 |
-
return raw.json();
|
| 22 |
-
|
| 23 |
-
}
|
| 24 |
|
| 25 |
export function SentimentWidget() {
|
| 26 |
const { text } = useTextStore();
|
| 27 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
return (
|
| 29 |
<section className="w-full max-w-2xl h-full flex flex-col">
|
| 30 |
<TitleBar label="Sentiment">
|
|
@@ -36,9 +28,8 @@ export function SentimentWidget() {
|
|
| 36 |
How?
|
| 37 |
</Link>
|
| 38 |
</TitleBar>
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
</Suspense>
|
| 42 |
</section>
|
| 43 |
|
| 44 |
)
|
|
|
|
| 1 |
'use client'
|
| 2 |
import { TitleBar } from "@/components/title-bar/TitleBar";
|
| 3 |
import { useTextStore } from "./TextStore";
|
|
|
|
| 4 |
import { SentimentList } from "./SentimentList";
|
| 5 |
import { Fallback } from "@/components/fallback/Fallback";
|
| 6 |
import Link from "next/link";
|
| 7 |
+
import { classify } from "@/util/classify";
|
| 8 |
+
import { useQuery } from "@tanstack/react-query";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
export function SentimentWidget() {
|
| 11 |
const { text } = useTextStore();
|
| 12 |
|
| 13 |
+
const { isPending, data: out } = useQuery({
|
| 14 |
+
queryKey: ['text', text],
|
| 15 |
+
queryFn: () =>
|
| 16 |
+
classify(text),
|
| 17 |
+
})
|
| 18 |
+
|
| 19 |
+
|
| 20 |
return (
|
| 21 |
<section className="w-full max-w-2xl h-full flex flex-col">
|
| 22 |
<TitleBar label="Sentiment">
|
|
|
|
| 28 |
How?
|
| 29 |
</Link>
|
| 30 |
</TitleBar>
|
| 31 |
+
{isPending ? <Fallback /> : <SentimentList out={out} />}
|
| 32 |
+
|
|
|
|
| 33 |
</section>
|
| 34 |
|
| 35 |
)
|
app/(index)/TextWidget.tsx
CHANGED
|
@@ -22,7 +22,7 @@ export function TextWidget() {
|
|
| 22 |
<textarea
|
| 23 |
onInput={(e) => debounced(e.currentTarget.value)}
|
| 24 |
defaultValue={text}
|
| 25 |
-
className="w-full h-full outline-none p-12 text-lg"
|
| 26 |
placeholder="Begin writing..."
|
| 27 |
/>
|
| 28 |
</section>
|
|
|
|
| 22 |
<textarea
|
| 23 |
onInput={(e) => debounced(e.currentTarget.value)}
|
| 24 |
defaultValue={text}
|
| 25 |
+
className="w-full min-h-52 md:min-h-0 h-full outline-none p-12 text-lg"
|
| 26 |
placeholder="Begin writing..."
|
| 27 |
/>
|
| 28 |
</section>
|
app/(index)/page.tsx
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
|
|
| 1 |
import { SentimentWidget } from "./SentimentWidget";
|
| 2 |
import { TextWidget } from "./TextWidget";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
export default function Home() {
|
| 5 |
return (
|
| 6 |
-
<
|
| 7 |
-
<
|
| 8 |
-
|
| 9 |
-
|
|
|
|
|
|
|
| 10 |
);
|
| 11 |
}
|
|
|
|
| 1 |
+
'use client'
|
| 2 |
import { SentimentWidget } from "./SentimentWidget";
|
| 3 |
import { TextWidget } from "./TextWidget";
|
| 4 |
+
import {
|
| 5 |
+
QueryClient,
|
| 6 |
+
QueryClientProvider,
|
| 7 |
+
} from '@tanstack/react-query'
|
| 8 |
+
|
| 9 |
+
const queryClient = new QueryClient();
|
| 10 |
|
| 11 |
export default function Home() {
|
| 12 |
return (
|
| 13 |
+
<QueryClientProvider client={queryClient}>
|
| 14 |
+
<div className="flex flex-col md:flex-row h-screen divide-x divide-zinc-200">
|
| 15 |
+
<TextWidget />
|
| 16 |
+
<SentimentWidget />
|
| 17 |
+
</div>
|
| 18 |
+
</QueryClientProvider>
|
| 19 |
);
|
| 20 |
}
|
app/api/classify/route.ts
CHANGED
|
@@ -10,7 +10,9 @@ export async function POST(request: NextRequest) {
|
|
| 10 |
}
|
| 11 |
const classifier = await PipelineSingleton.getInstance();
|
| 12 |
|
|
|
|
| 13 |
const result = await classifier(text, { top_k: null });
|
| 14 |
|
|
|
|
| 15 |
return NextResponse.json(result);
|
| 16 |
}
|
|
|
|
| 10 |
}
|
| 11 |
const classifier = await PipelineSingleton.getInstance();
|
| 12 |
|
| 13 |
+
//@ts-expect-error That's the library
|
| 14 |
const result = await classifier(text, { top_k: null });
|
| 15 |
|
| 16 |
+
|
| 17 |
return NextResponse.json(result);
|
| 18 |
}
|
components/title-bar/TitleBar.tsx
CHANGED
|
@@ -7,7 +7,10 @@ export interface ITitleBar extends React.ComponentPropsWithoutRef<'div'> {
|
|
| 7 |
|
| 8 |
export function TitleBar({ label, children, className, ...rest }: ITitleBar) {
|
| 9 |
return (
|
| 10 |
-
<div
|
|
|
|
|
|
|
|
|
|
| 11 |
<p className="font-medium text-xl">{label}</p>
|
| 12 |
{children}
|
| 13 |
</div >
|
|
|
|
| 7 |
|
| 8 |
export function TitleBar({ label, children, className, ...rest }: ITitleBar) {
|
| 9 |
return (
|
| 10 |
+
<div
|
| 11 |
+
className={cn("p-12 flex items-center justify-between border-y md:border-t-0 bg-zinc-50 md:bg-white border-zinc-200", className)}
|
| 12 |
+
{...rest}
|
| 13 |
+
>
|
| 14 |
<p className="font-medium text-xl">{label}</p>
|
| 15 |
{children}
|
| 16 |
</div >
|
next.config.ts
CHANGED
|
@@ -1,8 +1,24 @@
|
|
| 1 |
import type { NextConfig } from "next";
|
| 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
const nextConfig: NextConfig = {
|
| 4 |
/* config options here */
|
| 5 |
serverExternalPackages: ['sharp', 'onnxruntime-node'],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
};
|
| 7 |
|
| 8 |
export default nextConfig;
|
|
|
|
| 1 |
import type { NextConfig } from "next";
|
| 2 |
|
| 3 |
+
import path from "path";
|
| 4 |
+
import { fileURLToPath } from 'url';
|
| 5 |
+
|
| 6 |
+
const __filename = fileURLToPath(import.meta.url);
|
| 7 |
+
const __dirname = path.dirname(__filename);
|
| 8 |
+
|
| 9 |
const nextConfig: NextConfig = {
|
| 10 |
/* config options here */
|
| 11 |
serverExternalPackages: ['sharp', 'onnxruntime-node'],
|
| 12 |
+
webpack: (config) => {
|
| 13 |
+
config.resolve.alias['@huggingface/transformers'] = path.resolve(__dirname, 'node_modules/@huggingface/transformers');
|
| 14 |
+
config.resolve.alias = {
|
| 15 |
+
...config.resolve.alias,
|
| 16 |
+
"sharp$": false,
|
| 17 |
+
"onnxruntime-node$": false,
|
| 18 |
+
}
|
| 19 |
+
return config;
|
| 20 |
+
},
|
| 21 |
+
|
| 22 |
};
|
| 23 |
|
| 24 |
export default nextConfig;
|
package.json
CHANGED
|
@@ -10,13 +10,13 @@
|
|
| 10 |
},
|
| 11 |
"dependencies": {
|
| 12 |
"@huggingface/transformers": "^3.2.4",
|
|
|
|
| 13 |
"clsx": "^2.1.1",
|
| 14 |
"copy-webpack-plugin": "^12.0.2",
|
| 15 |
"next": "15.1.3",
|
| 16 |
"node-polyfill-webpack-plugin": "^4.1.0",
|
| 17 |
"react": "^19.0.0",
|
| 18 |
"react-dom": "^19.0.0",
|
| 19 |
-
"swr": "^2.3.0",
|
| 20 |
"use-debounce": "^10.0.4",
|
| 21 |
"words-count": "^2.0.2",
|
| 22 |
"zustand": "^5.0.2"
|
|
|
|
| 10 |
},
|
| 11 |
"dependencies": {
|
| 12 |
"@huggingface/transformers": "^3.2.4",
|
| 13 |
+
"@tanstack/react-query": "^5.62.15",
|
| 14 |
"clsx": "^2.1.1",
|
| 15 |
"copy-webpack-plugin": "^12.0.2",
|
| 16 |
"next": "15.1.3",
|
| 17 |
"node-polyfill-webpack-plugin": "^4.1.0",
|
| 18 |
"react": "^19.0.0",
|
| 19 |
"react-dom": "^19.0.0",
|
|
|
|
| 20 |
"use-debounce": "^10.0.4",
|
| 21 |
"words-count": "^2.0.2",
|
| 22 |
"zustand": "^5.0.2"
|
pipeline.ts
CHANGED
|
@@ -21,9 +21,12 @@ if (process.env.NODE_ENV !== 'production') {
|
|
| 21 |
// When running in development mode, attach the pipeline to the
|
| 22 |
// global object so that it's preserved between hot reloads.
|
| 23 |
// For more information, see https://vercel.com/guides/nextjs-prisma-postgres
|
|
|
|
| 24 |
if (!global.PipelineSingleton) {
|
|
|
|
| 25 |
global.PipelineSingleton = P();
|
| 26 |
}
|
|
|
|
| 27 |
PipelineSingleton = global.PipelineSingleton;
|
| 28 |
} else {
|
| 29 |
PipelineSingleton = P();
|
|
|
|
| 21 |
// When running in development mode, attach the pipeline to the
|
| 22 |
// global object so that it's preserved between hot reloads.
|
| 23 |
// For more information, see https://vercel.com/guides/nextjs-prisma-postgres
|
| 24 |
+
//@ts-expect-error I don't know how to define globalThis
|
| 25 |
if (!global.PipelineSingleton) {
|
| 26 |
+
//@ts-expect-error I don't know how to define globalThis
|
| 27 |
global.PipelineSingleton = P();
|
| 28 |
}
|
| 29 |
+
//@ts-expect-error I don't know how to define globalThis
|
| 30 |
PipelineSingleton = global.PipelineSingleton;
|
| 31 |
} else {
|
| 32 |
PipelineSingleton = P();
|
util/classify.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use server'
|
| 2 |
+
import PipelineSingleton from '@/pipeline';
|
| 3 |
+
|
| 4 |
+
export interface ClassifyOutputElement {
|
| 5 |
+
label: string;
|
| 6 |
+
score: number;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
export async function classify(text: string) {
|
| 10 |
+
const classifier = await PipelineSingleton.getInstance();
|
| 11 |
+
|
| 12 |
+
//@ts-expect-error That's the library
|
| 13 |
+
const out = await classifier(text, { top_k: null });
|
| 14 |
+
return out as ClassifyOutputElement[]
|
| 15 |
+
}
|
yarn.lock
CHANGED
|
@@ -444,6 +444,18 @@
|
|
| 444 |
dependencies:
|
| 445 |
tslib "^2.8.0"
|
| 446 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 447 |
"@types/estree@^1.0.6":
|
| 448 |
version "1.0.6"
|
| 449 |
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50"
|
|
@@ -1231,11 +1243,6 @@ define-properties@^1.1.3, define-properties@^1.2.1:
|
|
| 1231 |
has-property-descriptors "^1.0.0"
|
| 1232 |
object-keys "^1.1.1"
|
| 1233 |
|
| 1234 |
-
dequal@^2.0.3:
|
| 1235 |
-
version "2.0.3"
|
| 1236 |
-
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
|
| 1237 |
-
integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==
|
| 1238 |
-
|
| 1239 |
des.js@^1.0.0:
|
| 1240 |
version "1.1.0"
|
| 1241 |
resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.1.0.tgz#1d37f5766f3bbff4ee9638e871a8768c173b81da"
|
|
@@ -3562,14 +3569,6 @@ supports-preserve-symlinks-flag@^1.0.0:
|
|
| 3562 |
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
|
| 3563 |
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
|
| 3564 |
|
| 3565 |
-
swr@^2.3.0:
|
| 3566 |
-
version "2.3.0"
|
| 3567 |
-
resolved "https://registry.yarnpkg.com/swr/-/swr-2.3.0.tgz#66fa45023efd4199f4e7ce608c255709a135943d"
|
| 3568 |
-
integrity sha512-NyZ76wA4yElZWBHzSgEJc28a0u6QZvhb6w0azeL2k7+Q1gAzVK+IqQYXhVOC/mzi+HZIozrZvBVeSeOZNR2bqA==
|
| 3569 |
-
dependencies:
|
| 3570 |
-
dequal "^2.0.3"
|
| 3571 |
-
use-sync-external-store "^1.4.0"
|
| 3572 |
-
|
| 3573 |
tailwindcss@^3.4.1:
|
| 3574 |
version "3.4.17"
|
| 3575 |
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.17.tgz#ae8406c0f96696a631c790768ff319d46d5e5a63"
|
|
@@ -3780,11 +3779,6 @@ use-debounce@^10.0.4:
|
|
| 3780 |
resolved "https://registry.yarnpkg.com/use-debounce/-/use-debounce-10.0.4.tgz#2135be498ad855416c4495cfd8e0e130bd33bb24"
|
| 3781 |
integrity sha512-6Cf7Yr7Wk7Kdv77nnJMf6de4HuDE4dTxKij+RqE9rufDsI6zsbjyAxcH5y2ueJCQAnfgKbzXbZHYlkFwmBlWkw==
|
| 3782 |
|
| 3783 |
-
use-sync-external-store@^1.4.0:
|
| 3784 |
-
version "1.4.0"
|
| 3785 |
-
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz#adbc795d8eeb47029963016cefdf89dc799fcebc"
|
| 3786 |
-
integrity sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==
|
| 3787 |
-
|
| 3788 |
util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
|
| 3789 |
version "1.0.2"
|
| 3790 |
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
|
|
|
| 444 |
dependencies:
|
| 445 |
tslib "^2.8.0"
|
| 446 |
|
| 447 |
+
"@tanstack/query-core@5.62.15":
|
| 448 |
+
version "5.62.15"
|
| 449 |
+
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.62.15.tgz#ded0267ac31bd23f3c45ffc008dba85a25621e91"
|
| 450 |
+
integrity sha512-wT20X14CxcWY8YLJ/1pnsXn/y1Q2uRJZYWW93PWRtZt+3/JlGZyiyTcO4pGnqycnP7CokCROAyatsraosqZsDA==
|
| 451 |
+
|
| 452 |
+
"@tanstack/react-query@^5.62.15":
|
| 453 |
+
version "5.62.15"
|
| 454 |
+
resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.62.15.tgz#ecfe7187913e36c36630afedf4f8eff37247c400"
|
| 455 |
+
integrity sha512-Ny3xxsOWmEQCFyHiV3CF7t6+QAV+LpBEREiXyllKR4+tStyd8smOAa98ZHmEx0ZNy36M31K8enifB5wTSYAKJw==
|
| 456 |
+
dependencies:
|
| 457 |
+
"@tanstack/query-core" "5.62.15"
|
| 458 |
+
|
| 459 |
"@types/estree@^1.0.6":
|
| 460 |
version "1.0.6"
|
| 461 |
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50"
|
|
|
|
| 1243 |
has-property-descriptors "^1.0.0"
|
| 1244 |
object-keys "^1.1.1"
|
| 1245 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1246 |
des.js@^1.0.0:
|
| 1247 |
version "1.1.0"
|
| 1248 |
resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.1.0.tgz#1d37f5766f3bbff4ee9638e871a8768c173b81da"
|
|
|
|
| 3569 |
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
|
| 3570 |
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
|
| 3571 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3572 |
tailwindcss@^3.4.1:
|
| 3573 |
version "3.4.17"
|
| 3574 |
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.17.tgz#ae8406c0f96696a631c790768ff319d46d5e5a63"
|
|
|
|
| 3779 |
resolved "https://registry.yarnpkg.com/use-debounce/-/use-debounce-10.0.4.tgz#2135be498ad855416c4495cfd8e0e130bd33bb24"
|
| 3780 |
integrity sha512-6Cf7Yr7Wk7Kdv77nnJMf6de4HuDE4dTxKij+RqE9rufDsI6zsbjyAxcH5y2ueJCQAnfgKbzXbZHYlkFwmBlWkw==
|
| 3781 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3782 |
util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
|
| 3783 |
version "1.0.2"
|
| 3784 |
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|