Spaces:
Running
Running
refactoring
Browse files- package-lock.json +0 -0
- package.json +2 -1
- src/components/AgenticInterface.tsx +30 -151
- src/components/ChatSidebar.tsx +43 -0
- src/components/ColorControlForm.tsx +98 -0
- src/components/ColorVisualizer.tsx +25 -0
- src/components/MessageBubble.tsx +46 -0
- src/components/SidebarHeader.tsx +21 -0
- src/hooks/useTranslator.ts +38 -0
- src/lib/translationLanguages.ts +25 -0
- src/lib/translatorTypes.ts +6 -0
- src/worker.ts +26 -21
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 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 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 |
-
|
| 44 |
-
|
| 45 |
-
|
| 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 |
-
|
| 106 |
-
|
| 107 |
-
|
| 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 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 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 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 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
|
|
|
|
|
|
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
static model = 'Xenova/nllb-200-distilled-600M';
|
| 6 |
-
static instance: any = null;
|
| 7 |
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
}
|
| 13 |
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
});
|
| 18 |
|
| 19 |
-
const
|
| 20 |
-
|
| 21 |
-
|
|
|
|
| 22 |
});
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 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);
|
|
|
|
|
|