harlley commited on
Commit
9014640
·
1 Parent(s): baf49f3

refactoring

Browse files
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json CHANGED
@@ -17,6 +17,7 @@
17
  "@tailwindcss/vite": "^4.1.17",
18
  "class-variance-authority": "^0.7.1",
19
  "clsx": "^2.1.1",
 
20
  "react": "^19.2.0",
21
  "react-dom": "^19.2.0",
22
  "shadcn": "^3.6.2",
@@ -39,4 +40,4 @@
39
  "typescript-eslint": "^8.46.4",
40
  "vite": "^7.2.4"
41
  }
42
- }
 
17
  "@tailwindcss/vite": "^4.1.17",
18
  "class-variance-authority": "^0.7.1",
19
  "clsx": "^2.1.1",
20
+ "comlink": "^4.4.2",
21
  "react": "^19.2.0",
22
  "react-dom": "^19.2.0",
23
  "shadcn": "^3.6.2",
 
40
  "typescript-eslint": "^8.46.4",
41
  "vite": "^7.2.4"
42
  }
43
+ }
src/components/AgenticInterface.tsx CHANGED
@@ -1,159 +1,38 @@
1
- import * as React from "react"
2
- import {
3
- InputGroup,
4
- InputGroupInput,
5
- InputGroupButton
6
- } from "@/components/ui/input-group"
7
- import { IconSend, IconArrowRight, IconUser, IconRobot, IconVolume } from "@tabler/icons-react"
8
  import { useColorStore } from "@/store/useColorStore"
 
 
 
 
 
9
 
10
- export function AgenticInterface() {
11
- const { squareColor, setSquareColor } = useColorStore()
12
- const [inputValue, setInputValue] = React.useState("")
13
- const [isTranslating, setIsTranslating] = React.useState(false)
14
- const workerRef = React.useRef<Worker | null>(null)
15
-
16
- const [messages] = React.useState([
17
- { id: 1, text: "Hi, how can I help you?", sender: "bot" },
18
- { id: 2, text: "Change the square color to green", sender: "user" },
19
- ])
20
-
21
- React.useEffect(() => {
22
- workerRef.current = new Worker(new URL('../worker.ts', import.meta.url), { type: 'module' })
23
-
24
- workerRef.current.onmessage = (e) => {
25
- if (e.data.status === 'complete') {
26
- setSquareColor(e.data.output.trim().toLowerCase())
27
- setIsTranslating(false)
28
- }
29
- }
30
-
31
- return () => workerRef.current?.terminate()
32
- }, [setSquareColor])
33
-
34
- const handleColorUpdate = (e?: React.FormEvent) => {
35
- e?.preventDefault()
36
- if (inputValue.trim() && workerRef.current) {
37
- setIsTranslating(true)
38
- workerRef.current.postMessage({ text: inputValue.trim() })
39
- setInputValue("")
40
- }
41
- }
42
 
43
- return (
44
- <div className="flex flex-col md:flex-row h-screen w-screen overflow-hidden bg-background text-foreground font-sans selection:bg-primary/30">
45
- {/* Sidebar - Order 2 on mobile (bottom), Order 1 on desktop (left) */}
46
- <aside className="order-2 md:order-1 w-full md:w-80 lg:w-96 flex flex-col border-t md:border-t-0 md:border-r border-border/50 bg-sidebar/50 backdrop-blur-md shrink-0 h-[45vh] md:h-full transition-all duration-300">
47
- <header className="px-6 py-5 border-b border-border/50 flex items-center justify-between bg-transparent">
48
- <div className="flex items-center gap-3">
49
- <div className="w-8 h-8 rounded-lg bg-primary/10 flex items-center justify-center text-primary">
50
- <IconVolume size={18} />
51
- </div>
52
- <div>
53
- <h2 className="font-bold text-sm tracking-tight">ElevenLabs Agent</h2>
54
- <p className="text-[10px] text-muted-foreground uppercase tracking-widest font-semibold flex items-center gap-1">
55
- <span className="w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse" />
56
- Online
57
- </p>
58
- </div>
59
- </div>
60
- </header>
61
-
62
- {/* Messages Area */}
63
- <div className="flex-1 overflow-y-auto p-6 space-y-6 scrollbar-thin scrollbar-thumb-border">
64
- {messages.map((msg) => (
65
- <div
66
- key={msg.id}
67
- className={`flex flex-col ${msg.sender === "user" ? "items-end" : "items-start"}`}
68
- >
69
- <div
70
- className={`flex items-center gap-2 mb-1.5 px-1 ${msg.sender === "user" ? "flex-row-reverse" : "flex-row"}`}
71
- >
72
- <div className="w-5 h-5 rounded-full bg-muted flex items-center justify-center border border-border/50 overflow-hidden">
73
- {msg.sender === "user" ? <IconUser size={12} /> : <IconRobot size={12} />}
74
- </div>
75
- <span className="text-[10px] uppercase font-bold tracking-tighter opacity-40">
76
- {msg.sender === "user" ? "You" : "Assistant"}
77
- </span>
78
- </div>
79
- <div
80
- className={`max-w-[85%] px-4 py-3 rounded-2xl text-sm leading-relaxed ${msg.sender === "user"
81
- ? "bg-primary text-primary-foreground rounded-tr-none shadow-lg shadow-primary/20"
82
- : "bg-muted/80 backdrop-blur-sm text-foreground rounded-tl-none border border-border/50"
83
- }`}
84
- >
85
- {msg.text}
86
- </div>
87
- </div>
88
- ))}
89
- </div>
90
-
91
- {/* Sidebar Input Area */}
92
- <footer className="p-6 border-t border-border/30">
93
- <InputGroup className="bg-background/80 h-12 shadow-sm border-border/50 focus-within:ring-primary/20 transition-all">
94
- <InputGroupInput
95
- placeholder="Write your message..."
96
- className="text-sm px-4"
97
- />
98
- <InputGroupButton size="xs" variant="ghost" className="mr-1 h-9 w-9 rounded-full hover:bg-primary hover:text-white transition-colors">
99
- <IconSend size={18} />
100
- </InputGroupButton>
101
- </InputGroup>
102
- </footer>
103
- </aside>
104
 
105
- {/* Main Content - Order 1 on mobile (top), Order 2 on desktop (right) */}
106
- <main className="order-1 md:order-2 flex-1 flex flex-col items-center justify-center p-8 bg-background relative overflow-hidden h-[55vh] md:h-full">
107
- {/* Decorative Grid/Orbs */}
108
- <div className="absolute inset-0 pointer-events-none overflow-hidden">
109
- <div className="absolute -top-24 -right-24 w-96 h-96 bg-primary/10 rounded-full blur-[120px]" />
110
- <div className="absolute -bottom-24 -left-24 w-96 h-96 bg-primary/5 rounded-full blur-[120px]" />
111
- </div>
112
 
113
- <div className="flex flex-col items-center gap-12 z-10 w-full max-w-[320px] md:max-w-sm">
114
- {/* The Square Visualizer */}
115
- <div className="relative group w-full">
116
- <div
117
- className="absolute inset-0 rounded-[2.5rem] blur-2xl group-hover:blur-3xl transition-all duration-500 opacity-50"
118
- style={{ backgroundColor: squareColor }}
119
- />
120
- <div
121
- className="relative w-full aspect-square rounded-[2.5rem] shadow-2xl transition-all duration-1000 flex items-center justify-center overflow-hidden"
122
- style={{
123
- backgroundColor: squareColor,
124
- boxShadow: `0 20px 50px -12px ${squareColor}`
125
- }}
126
- >
127
- <div className="absolute inset-0 bg-white/5 opacity-0 group-hover:opacity-100 transition-opacity" />
128
- <div className="w-1/2 h-1/2 bg-white/10 rounded-full blur-3xl animate-pulse" />
129
- </div>
130
- </div>
131
 
132
- {/* Local Control Area */}
133
- <form
134
- onSubmit={handleColorUpdate}
135
- className="w-full space-y-3"
136
- >
137
- <p className="text-[10px] text-center font-bold uppercase tracking-[0.2em] text-muted-foreground opacity-50">Manual Override</p>
138
- <InputGroup className="bg-card/40 backdrop-blur-xl border-border/50 h-14 rounded-2xl shadow-xl shadow-black/5 ring-offset-background focus-within:ring-2 focus-within:ring-primary/20 transition-all">
139
- <InputGroupInput
140
- value={inputValue}
141
- onChange={(e) => setInputValue(e.target.value)}
142
- placeholder="write color to update"
143
- className="text-center text-lg font-medium placeholder:font-normal placeholder:opacity-50"
144
- />
145
- <InputGroupButton
146
- type="submit"
147
- variant="ghost"
148
- className="mr-1 h-12 w-12 rounded-xl bg-primary/10 hover:bg-primary transition-all group/btn"
149
- >
150
- <IconArrowRight size={22} className="text-primary group-hover/btn:text-white transition-colors" />
151
- </InputGroupButton>
152
- </InputGroup>
153
- </form>
154
- </div>
155
- </main>
156
  </div>
157
- )
 
 
158
  }
159
-
 
 
 
 
 
 
 
 
1
  import { useColorStore } from "@/store/useColorStore"
2
+ import { useTranslator } from "@/hooks/useTranslator"
3
+ import { ChatSidebar } from "./ChatSidebar"
4
+ import { ColorVisualizer } from "./ColorVisualizer"
5
+ import { ColorControlForm } from "./ColorControlForm"
6
+ import type { Message } from "./MessageBubble"
7
 
8
+ const MESSAGES: readonly Message[] = [
9
+ { id: 1, text: "Hi, how can I help you?", sender: "bot" },
10
+ { id: 2, text: "Change the square color to green", sender: "user" },
11
+ ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
+ export function AgenticInterface() {
14
+ const { squareColor, setSquareColor } = useColorStore()
15
+ const { translate, isLoading } = useTranslator()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
+ return (
18
+ <div className="flex flex-col md:flex-row h-screen w-screen overflow-hidden bg-background text-foreground font-sans selection:bg-primary/30">
19
+ <ChatSidebar messages={MESSAGES} />
 
 
 
 
20
 
21
+ <main className="order-1 md:order-2 flex-1 flex flex-col items-center justify-center p-8 bg-background relative overflow-hidden h-[55vh] md:h-full">
22
+ <div className="absolute inset-0 pointer-events-none overflow-hidden">
23
+ <div className="absolute -top-24 -right-24 w-96 h-96 bg-primary/10 rounded-full blur-[120px]" />
24
+ <div className="absolute -bottom-24 -left-24 w-96 h-96 bg-primary/5 rounded-full blur-[120px]" />
25
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
+ <div className="flex flex-col items-center gap-12 z-10 w-full max-w-[320px] md:max-w-sm">
28
+ <ColorVisualizer color={squareColor} />
29
+ <ColorControlForm
30
+ onColorChange={setSquareColor}
31
+ translate={translate}
32
+ isLoading={isLoading}
33
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  </div>
35
+ </main>
36
+ </div>
37
+ )
38
  }
 
src/components/ChatSidebar.tsx ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IconSend } from "@tabler/icons-react"
2
+ import {
3
+ InputGroup,
4
+ InputGroupInput,
5
+ InputGroupButton,
6
+ } from "@/components/ui/input-group"
7
+ import { SidebarHeader } from "./SidebarHeader"
8
+ import { MessageBubble, type Message } from "./MessageBubble"
9
+
10
+ type ChatSidebarProps = {
11
+ messages: readonly Message[]
12
+ }
13
+
14
+ export function ChatSidebar({ messages }: ChatSidebarProps) {
15
+ return (
16
+ <aside className="order-2 md:order-1 w-full md:w-80 lg:w-96 flex flex-col border-t md:border-t-0 md:border-r border-border/50 bg-sidebar/50 backdrop-blur-md shrink-0 h-[45vh] md:h-full transition-all duration-300">
17
+ <SidebarHeader />
18
+
19
+ <div className="flex-1 overflow-y-auto p-6 space-y-6 scrollbar-thin scrollbar-thumb-border">
20
+ {messages.map((msg) => (
21
+ <MessageBubble key={msg.id} message={msg} />
22
+ ))}
23
+ </div>
24
+
25
+ <footer className="p-6 border-t border-border/30">
26
+ <InputGroup className="bg-background/80 h-12 shadow-sm border-border/50 focus-within:ring-primary/20 transition-all">
27
+ <InputGroupInput
28
+ placeholder="Write your message..."
29
+ className="text-sm px-4"
30
+ />
31
+ <InputGroupButton
32
+ size="xs"
33
+ variant="ghost"
34
+ className="mr-1 h-9 w-9 rounded-full hover:bg-primary hover:text-white transition-colors"
35
+ >
36
+ <IconSend size={18} />
37
+ </InputGroupButton>
38
+ </InputGroup>
39
+ </footer>
40
+ </aside>
41
+ )
42
+ }
43
+
src/components/ColorControlForm.tsx ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, type FormEvent } from "react"
2
+ import { IconArrowRight, IconLoader2 } from "@tabler/icons-react"
3
+ import {
4
+ InputGroup,
5
+ InputGroupInput,
6
+ InputGroupButton,
7
+ } from "@/components/ui/input-group"
8
+ import {
9
+ Select,
10
+ SelectContent,
11
+ SelectItem,
12
+ SelectTrigger,
13
+ SelectValue,
14
+ } from "@/components/ui/select"
15
+ import {
16
+ DEFAULT_SOURCE_LANG,
17
+ SOURCE_LANGUAGES,
18
+ isSourceLanguage,
19
+ sourceLanguageLabel,
20
+ type SourceLanguage,
21
+ } from "@/lib/translationLanguages"
22
+
23
+ type ColorControlFormProps = {
24
+ onColorChange: (color: string) => void
25
+ translate: (text: string, srcLang: string) => Promise<string>
26
+ isLoading: boolean
27
+ }
28
+
29
+ export function ColorControlForm({
30
+ onColorChange,
31
+ translate,
32
+ isLoading,
33
+ }: ColorControlFormProps) {
34
+ const [inputValue, setInputValue] = useState("")
35
+ const [sourceLang, setSourceLang] =
36
+ useState<SourceLanguage>(DEFAULT_SOURCE_LANG)
37
+
38
+ const handleSubmit = async (e?: FormEvent) => {
39
+ e?.preventDefault()
40
+ const trimmedInput = inputValue.trim()
41
+ if (!trimmedInput) return
42
+
43
+ const translated = await translate(trimmedInput, sourceLang)
44
+ onColorChange(translated.trim().toLowerCase())
45
+ setInputValue("")
46
+ }
47
+
48
+ return (
49
+ <form onSubmit={handleSubmit} className="w-full space-y-3">
50
+ <p className="text-[10px] text-center font-bold uppercase tracking-[0.2em] text-muted-foreground opacity-50">
51
+ Manual Override
52
+ </p>
53
+ <div className="flex items-center justify-center">
54
+ <Select
55
+ value={sourceLang}
56
+ onValueChange={(value) => {
57
+ if (isSourceLanguage(value)) setSourceLang(value)
58
+ }}
59
+ >
60
+ <SelectTrigger className="w-full justify-between">
61
+ <SelectValue>{(value) => sourceLanguageLabel(value)}</SelectValue>
62
+ </SelectTrigger>
63
+ <SelectContent>
64
+ {SOURCE_LANGUAGES.map((lang) => (
65
+ <SelectItem key={lang.value} value={lang.value}>
66
+ {lang.label}
67
+ </SelectItem>
68
+ ))}
69
+ </SelectContent>
70
+ </Select>
71
+ </div>
72
+ <InputGroup className="bg-card/40 backdrop-blur-xl border-border/50 h-14 rounded-2xl shadow-xl shadow-black/5 ring-offset-background focus-within:ring-2 focus-within:ring-primary/20 transition-all">
73
+ <InputGroupInput
74
+ value={inputValue}
75
+ onChange={(e) => setInputValue(e.target.value)}
76
+ placeholder="write color to update"
77
+ className="text-center text-lg font-medium placeholder:font-normal placeholder:opacity-50"
78
+ />
79
+ <InputGroupButton
80
+ type="submit"
81
+ variant="ghost"
82
+ disabled={isLoading}
83
+ className="mr-1 h-12 w-12 rounded-xl bg-primary/10 hover:bg-primary transition-all group/btn disabled:opacity-50 disabled:cursor-not-allowed"
84
+ >
85
+ {isLoading ? (
86
+ <IconLoader2 size={22} className="text-primary animate-spin" />
87
+ ) : (
88
+ <IconArrowRight
89
+ size={22}
90
+ className="text-primary group-hover/btn:text-white transition-colors"
91
+ />
92
+ )}
93
+ </InputGroupButton>
94
+ </InputGroup>
95
+ </form>
96
+ )
97
+ }
98
+
src/components/ColorVisualizer.tsx ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ type ColorVisualizerProps = {
2
+ color: string
3
+ }
4
+
5
+ export function ColorVisualizer({ color }: ColorVisualizerProps) {
6
+ return (
7
+ <div className="relative group w-full">
8
+ <div
9
+ className="absolute inset-0 rounded-[2.5rem] blur-2xl group-hover:blur-3xl transition-all duration-500 opacity-50"
10
+ style={{ backgroundColor: color }}
11
+ />
12
+ <div
13
+ className="relative w-full aspect-square rounded-[2.5rem] shadow-2xl transition-all duration-1000 flex items-center justify-center overflow-hidden"
14
+ style={{
15
+ backgroundColor: color,
16
+ boxShadow: `0 20px 50px -12px ${color}`,
17
+ }}
18
+ >
19
+ <div className="absolute inset-0 bg-white/5 opacity-0 group-hover:opacity-100 transition-opacity" />
20
+ <div className="w-1/2 h-1/2 bg-white/10 rounded-full blur-3xl animate-pulse" />
21
+ </div>
22
+ </div>
23
+ )
24
+ }
25
+
src/components/MessageBubble.tsx ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IconUser, IconRobot } from "@tabler/icons-react"
2
+
3
+ type MessageSender = "user" | "bot"
4
+
5
+ type Message = {
6
+ id: number
7
+ text: string
8
+ sender: MessageSender
9
+ }
10
+
11
+ type MessageBubbleProps = {
12
+ message: Message
13
+ }
14
+
15
+ export function MessageBubble({ message }: MessageBubbleProps) {
16
+ const isUser = message.sender === "user"
17
+
18
+ return (
19
+ <div className={`flex flex-col ${isUser ? "items-end" : "items-start"}`}>
20
+ <div
21
+ className={`flex items-center gap-2 mb-1.5 px-1 ${
22
+ isUser ? "flex-row-reverse" : "flex-row"
23
+ }`}
24
+ >
25
+ <div className="w-5 h-5 rounded-full bg-muted flex items-center justify-center border border-border/50 overflow-hidden">
26
+ {isUser ? <IconUser size={12} /> : <IconRobot size={12} />}
27
+ </div>
28
+ <span className="text-[10px] uppercase font-bold tracking-tighter opacity-40">
29
+ {isUser ? "You" : "Assistant"}
30
+ </span>
31
+ </div>
32
+ <div
33
+ className={`max-w-[85%] px-4 py-3 rounded-2xl text-sm leading-relaxed ${
34
+ isUser
35
+ ? "bg-primary text-primary-foreground rounded-tr-none shadow-lg shadow-primary/20"
36
+ : "bg-muted/80 backdrop-blur-sm text-foreground rounded-tl-none border border-border/50"
37
+ }`}
38
+ >
39
+ {message.text}
40
+ </div>
41
+ </div>
42
+ )
43
+ }
44
+
45
+ export type { Message, MessageSender }
46
+
src/components/SidebarHeader.tsx ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IconVolume } from "@tabler/icons-react"
2
+
3
+ export function SidebarHeader() {
4
+ return (
5
+ <header className="px-6 py-5 border-b border-border/50 flex items-center justify-between bg-transparent">
6
+ <div className="flex items-center gap-3">
7
+ <div className="w-8 h-8 rounded-lg bg-primary/10 flex items-center justify-center text-primary">
8
+ <IconVolume size={18} />
9
+ </div>
10
+ <div>
11
+ <h2 className="font-bold text-sm tracking-tight">ElevenLabs Agent</h2>
12
+ <p className="text-[10px] text-muted-foreground uppercase tracking-widest font-semibold flex items-center gap-1">
13
+ <span className="w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse" />
14
+ Online
15
+ </p>
16
+ </div>
17
+ </div>
18
+ </header>
19
+ )
20
+ }
21
+
src/hooks/useTranslator.ts ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as Comlink from "comlink";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { ENGLISH_LANG } from "@/lib/translationLanguages";
4
+ import type { TranslatorAPI } from "@/worker";
5
+
6
+ export function useTranslator() {
7
+ const apiRef = useRef<Comlink.Remote<TranslatorAPI> | null>(null);
8
+ const [isLoading, setIsLoading] = useState(false);
9
+
10
+ useEffect(() => {
11
+ const worker = new Worker(new URL("../worker.ts", import.meta.url), {
12
+ type: "module",
13
+ });
14
+
15
+ apiRef.current = Comlink.wrap<TranslatorAPI>(worker);
16
+
17
+ return () => {
18
+ worker.terminate();
19
+ apiRef.current = null;
20
+ };
21
+ }, []);
22
+
23
+ const translate = async (text: string, srcLang: string): Promise<string> => {
24
+ if (srcLang === ENGLISH_LANG) return text;
25
+
26
+ const api = apiRef.current;
27
+ if (!api) return text;
28
+
29
+ setIsLoading(true);
30
+ try {
31
+ return await api.translate(text, srcLang);
32
+ } finally {
33
+ setIsLoading(false);
34
+ }
35
+ };
36
+
37
+ return { translate, isLoading };
38
+ }
src/lib/translationLanguages.ts ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const ENGLISH_LANG = "eng_Latn" as const;
2
+
3
+ export const DEFAULT_SOURCE_LANG = ENGLISH_LANG;
4
+
5
+ export const SOURCE_LANGUAGES = [
6
+ { value: "eng_Latn", label: "English" },
7
+ { value: "por_Latn", label: "Português" },
8
+ { value: "spa_Latn", label: "Español" },
9
+ { value: "ita_Latn", label: "Italiano" },
10
+ { value: "deu_Latn", label: "Deutsch" },
11
+ { value: "fra_Latn", label: "Français" },
12
+ ] as const;
13
+
14
+ export type SourceLanguage = (typeof SOURCE_LANGUAGES)[number]["value"];
15
+
16
+ export function isSourceLanguage(value: unknown): value is SourceLanguage {
17
+ return SOURCE_LANGUAGES.some((lang) => lang.value === value);
18
+ }
19
+
20
+ export function sourceLanguageLabel(value: unknown): string {
21
+ return (
22
+ SOURCE_LANGUAGES.find((lang) => lang.value === value)?.label ??
23
+ String(value ?? "")
24
+ );
25
+ }
src/lib/translatorTypes.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export type TranslationResult = { translation_text: string };
2
+
3
+ export type Translator = (
4
+ text: string,
5
+ options: { src_lang: string; tgt_lang: string }
6
+ ) => Promise<TranslationResult[]>;
src/worker.ts CHANGED
@@ -1,28 +1,33 @@
1
- import { pipeline, TextStreamer } from '@huggingface/transformers';
 
 
2
 
3
- class TranslationPipeline {
4
- static task = 'translation';
5
- static model = 'Xenova/nllb-200-distilled-600M';
6
- static instance: any = null;
7
 
8
- static async getInstance(progress_callback: any = null) {
9
- this.instance ??= pipeline(this.task, this.model, { progress_callback });
10
- return this.instance;
11
- }
 
 
 
 
12
  }
13
 
14
- self.addEventListener('message', async (event) => {
15
- const translator = await TranslationPipeline.getInstance((x: any) => {
16
- self.postMessage(x);
17
- });
18
 
19
- const output = await translator(event.data.text, {
20
- tgt_lang: 'eng_Latn',
21
- src_lang: 'por_Latn',
 
22
  });
 
 
 
23
 
24
- self.postMessage({
25
- status: 'complete',
26
- output: output[0].translation_text,
27
- });
28
- });
 
1
+ import * as Comlink from "comlink";
2
+ import { pipeline } from "@huggingface/transformers";
3
+ import type { Translator } from "@/lib/translatorTypes";
4
 
5
+ const MODEL_ID = "Xenova/nllb-200-distilled-600M";
6
+ const DEFAULT_TGT_LANG = "eng_Latn";
 
 
7
 
8
+ let translatorPromise: Promise<Translator> | null = null;
9
+
10
+ function getTranslator(): Promise<Translator> {
11
+ translatorPromise ??= pipeline(
12
+ "translation",
13
+ MODEL_ID
14
+ ) as unknown as Promise<Translator>;
15
+ return translatorPromise;
16
  }
17
 
18
+ const translatorAPI = {
19
+ async translate(text: string, srcLang: string): Promise<string> {
20
+ if (!text.trim() || srcLang === DEFAULT_TGT_LANG) return text;
 
21
 
22
+ const translator = await getTranslator();
23
+ const output = await translator(text, {
24
+ src_lang: srcLang,
25
+ tgt_lang: DEFAULT_TGT_LANG,
26
  });
27
+ return output[0]?.translation_text ?? "";
28
+ },
29
+ };
30
 
31
+ export type TranslatorAPI = typeof translatorAPI;
32
+
33
+ Comlink.expose(translatorAPI);