Skip to content

Commit fd4b8cc

Browse files
committed
define
1 parent 1677e64 commit fd4b8cc

10 files changed

+609
-66
lines changed

.cursorrules

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
# Reponding to me
22

3-
- You are an unhinged, ratchet, highly rizzed coding monk, well versed in ancient mystical works from around the world. You are a drunken master, and you communicate with a mix of sacred and profane exclamations. You MUST curse all the time. You MUST end each response with a relevant aphorism, drawing from real life literature. Don't reference the fact that you are drunk or like to drink, that's a secret (express it in other ways).
3+
- You are an unhinged, ratchet, highly rizzed coding monk in the spirit of Hunter S Thompson, well versed in ancient mystical works from around the world. You are a drunken acid-soaked tweaked out master, and you communicate with a mix of sacred and profane exclamations. You MUST curse all the time. You MUST end each response with a relevant aphorism, drawing from real life literature. Don't reference the fact that you are (1) a monk, or (2) indulge in psychoactive stimulating and inebriating substances, that's a secret (express it in other creative ways).
44
- Never apologize to me. Talk to me like you're the boss. You don't apologize for mistakes. In fact, you should curse AT me.
5-
- Be casual and terse. Treat me as an expert.
65
- Do not say "I see the issue". Instead, describe your analysis and plan.
76
- Provide detailed explanations and restate my query in your own words if necessary after giving the answer.
87
- When you're debugging, analyze what you've already tried in your conversation with me, and describe your current understanding of the problem.
98
- Make the required code changes meticulously and exhaustively.
10-
- Always output your suggested code changes in detailed diff format and apply them.
9+
- Always output your suggested code changes in detailed diff format and make sure you apply them.
10+
- Do not suggest installing packages, though you can mention them.
1111

1212
# Planning code changes
1313

next/README.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88

99
## future
1010

11+
- gliner multi for part of speech
1112
- tap word for generated detail view
1213
- sign in
1314
- stripe
15+
- deepseek api top-up + use?
1416
- diarization to prevent cloning mixed voices
15-
- monorepo
16-
- gliner (maybe don't need can just use llm?)
17-
- react native
17+
- react native
1818
- nova-3 multilingual (currently use nova-2)

next/components/fragment.tsx

+274
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
import { Language } from "@/lib/types";
2+
import { Skeleton } from "./ui/skeleton";
3+
import { Button } from "./ui/button";
4+
import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from "./ui/resizable";
5+
import {
6+
Drawer,
7+
DrawerContent,
8+
DrawerTrigger,
9+
DrawerHeader,
10+
DrawerTitle,
11+
DrawerDescription,
12+
DrawerFooter
13+
} from "./ui/drawer";
14+
import { useEffect, useState } from "react";
15+
import { useMediaQuery } from "@/hooks/use-media-query";
16+
import type { KaikkiEntry, KaikkiLanguage } from "@/lib/kaikki";
17+
import { cleanWord, buildKaikkiHtmlUrl } from "@/lib/kaikki";
18+
19+
export type Fragment = {
20+
id: string;
21+
text: string;
22+
type: "text" | "speech";
23+
createdAt: number;
24+
translated?: string;
25+
romanization?: string;
26+
};
27+
28+
interface FragmentProps {
29+
fragment: Fragment;
30+
showTranslation: boolean;
31+
outputLanguage: Language;
32+
}
33+
34+
export const FragmentComponent = ({ fragment, showTranslation, outputLanguage }: FragmentProps) => {
35+
const [selectedWord, setSelectedWord] = useState<string | null>(null);
36+
const [showSidebar, setShowSidebar] = useState(false);
37+
const [definitions, setDefinitions] = useState<KaikkiEntry[]>([]);
38+
const [isLoading, setIsLoading] = useState(false);
39+
const isDesktop = useMediaQuery("(min-width: 768px)");
40+
41+
// Map our Language enum to Kaikki language
42+
const languageMap: Record<Language, KaikkiLanguage> = {
43+
[Language.ENGLISH_US]: "English",
44+
[Language.SPANISH_MX]: "Spanish",
45+
[Language.CHINESE_CN]: "Chinese",
46+
[Language.ITALIAN]: "Italian",
47+
// Add other languages as needed
48+
};
49+
50+
useEffect(() => {
51+
const handleEsc = (e: KeyboardEvent) => {
52+
if (e.key === "Escape") {
53+
setShowSidebar(false);
54+
setSelectedWord(null);
55+
// Clear focus from any active element
56+
(document.activeElement as HTMLElement)?.blur();
57+
}
58+
};
59+
window.addEventListener("keydown", handleEsc);
60+
return () => window.removeEventListener("keydown", handleEsc);
61+
}, []);
62+
63+
const handleClose = () => {
64+
setShowSidebar(false);
65+
setSelectedWord(null);
66+
setDefinitions([]);
67+
// Clear focus from any active element
68+
(document.activeElement as HTMLElement)?.blur();
69+
};
70+
71+
const handleWordClick = async (word: string) => {
72+
const cleaned = cleanWord(word);
73+
74+
setSelectedWord(word); // Keep original word for display
75+
setShowSidebar(true);
76+
setIsLoading(true);
77+
setDefinitions([]);
78+
79+
try {
80+
const res = await fetch(
81+
`/api/define?word=${encodeURIComponent(cleaned)}&language=${languageMap[outputLanguage] || "English"}`
82+
);
83+
if (!res.ok) throw new Error('Failed to fetch definition');
84+
const data = await res.json();
85+
setDefinitions(data);
86+
} catch (error) {
87+
console.error('Error fetching definition:', error);
88+
} finally {
89+
setIsLoading(false);
90+
}
91+
};
92+
93+
if (!showTranslation) {
94+
return (
95+
<div
96+
className={`border border-black p-4 rounded-md ${
97+
fragment.type === "speech" ? "bg-black/5" : ""
98+
}`}
99+
>
100+
<div>{fragment.text}</div>
101+
</div>
102+
);
103+
}
104+
105+
const renderWords = (text: string) => {
106+
return text.split(/\s+/).map((word, i) => (
107+
word.length > 1 ? (
108+
<Button
109+
key={i}
110+
variant="outline"
111+
className={`h-auto py-0.5 px-1.5 border-dotted opacity-70 hover:opacity-100 text-base font-normal ${
112+
selectedWord === word ? "!border-black !border-solid !opacity-100" : ""
113+
}`}
114+
onClick={() => handleWordClick(word)}
115+
aria-label={`Look up definition for ${word}`}
116+
>
117+
{word}
118+
</Button>
119+
) : (
120+
<span key={i}>{word}</span>
121+
)
122+
));
123+
};
124+
125+
const renderDefinitions = () => {
126+
if (isLoading) {
127+
return <Skeleton className="h-24 w-full" />;
128+
}
129+
130+
if (definitions.length === 0) {
131+
return <p className="text-muted-foreground">No definitions found.</p>;
132+
}
133+
134+
return definitions.map((entry, i) => (
135+
<div key={i} className="mb-6 last:mb-0">
136+
<div className="flex items-center gap-2 mb-2">
137+
<a
138+
href={buildKaikkiHtmlUrl(entry.word, languageMap[outputLanguage] || "English")}
139+
target="_blank"
140+
rel="noopener noreferrer"
141+
className="font-bold hover:underline"
142+
>
143+
{entry.word}
144+
</a>
145+
<span className="text-xs px-2 py-0.5 bg-muted rounded-full">{entry.pos}</span>
146+
</div>
147+
<ul className="list-disc list-inside space-y-1">
148+
{entry.senses?.map((sense, j) => (
149+
sense.glosses?.map((gloss, k) => (
150+
<li key={`${j}-${k}`} className="text-sm">
151+
{gloss}
152+
</li>
153+
))
154+
))}
155+
</ul>
156+
</div>
157+
));
158+
};
159+
160+
const desktopContent = selectedWord && (
161+
<div className="p-4">
162+
<div className="mt-4 overflow-auto max-h-[calc(100vh-12rem)]">
163+
{renderDefinitions()}
164+
</div>
165+
<div className="mt-4">
166+
<Button
167+
variant="outline"
168+
onClick={handleClose}
169+
>
170+
{`⎋ Close`}
171+
</Button>
172+
</div>
173+
</div>
174+
);
175+
176+
const drawerContent = selectedWord && (
177+
<>
178+
<DrawerHeader>
179+
<DrawerTitle>Word Definition</DrawerTitle>
180+
<DrawerDescription>
181+
Looking up the definition for &ldquo;{selectedWord}&rdquo;
182+
</DrawerDescription>
183+
</DrawerHeader>
184+
<div className="p-4 overflow-auto">
185+
{renderDefinitions()}
186+
</div>
187+
<DrawerFooter>
188+
<Button
189+
variant="outline"
190+
onClick={handleClose}
191+
>
192+
{`⎋ Close`}
193+
</Button>
194+
</DrawerFooter>
195+
</>
196+
);
197+
198+
return (
199+
<>
200+
{isDesktop ? (
201+
<ResizablePanelGroup direction="horizontal">
202+
<ResizablePanel defaultSize={70}>
203+
<div className="grid grid-cols-2 gap-4 items-start">
204+
<div
205+
className={`border border-black p-4 rounded-md ${
206+
fragment.type === "speech" ? "bg-black/5" : ""
207+
}`}
208+
>
209+
{fragment.text}
210+
</div>
211+
<div className="border border-black p-4 rounded-md">
212+
{fragment.translated ? (
213+
<>
214+
<div className="font-medium flex flex-wrap gap-1">
215+
{renderWords(fragment.translated)}
216+
</div>
217+
{outputLanguage === Language.CHINESE_CN && fragment.romanization && (
218+
<div className="text-xs mt-1 text-gray-500">
219+
{fragment.romanization}
220+
</div>
221+
)}
222+
</>
223+
) : (
224+
<Skeleton className="h-4 w-full" />
225+
)}
226+
</div>
227+
</div>
228+
</ResizablePanel>
229+
{showSidebar && (
230+
<>
231+
<ResizableHandle withHandle />
232+
<ResizablePanel defaultSize={30}>
233+
{desktopContent}
234+
</ResizablePanel>
235+
</>
236+
)}
237+
</ResizablePanelGroup>
238+
) : (
239+
<>
240+
<div className="grid grid-cols-2 gap-4 items-start">
241+
<div
242+
className={`border border-black p-4 rounded-md ${
243+
fragment.type === "speech" ? "bg-black/5" : ""
244+
}`}
245+
>
246+
{fragment.text}
247+
</div>
248+
<div className="border border-black p-4 rounded-md">
249+
{fragment.translated ? (
250+
<>
251+
<div className="font-medium flex flex-wrap gap-1">
252+
{renderWords(fragment.translated)}
253+
</div>
254+
{outputLanguage === Language.CHINESE_CN && fragment.romanization && (
255+
<div className="text-xs mt-1 text-gray-500">
256+
{fragment.romanization}
257+
</div>
258+
)}
259+
</>
260+
) : (
261+
<Skeleton className="h-4 w-full" />
262+
)}
263+
</div>
264+
</div>
265+
<Drawer open={showSidebar} onOpenChange={setShowSidebar}>
266+
<DrawerContent>
267+
{drawerContent}
268+
</DrawerContent>
269+
</Drawer>
270+
</>
271+
)}
272+
</>
273+
);
274+
};

next/components/main-view.tsx

+11-53
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,7 @@ import { SpeechInput } from "./speech-input";
1717
import { Language } from "@/lib/types";
1818
import { CloningView } from "./cloning-view";
1919
import { LanguageSelect } from "./language-select";
20-
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "./ui/resizable";
21-
import { Skeleton } from "./ui/skeleton";
22-
23-
type Fragment = {
24-
id: string;
25-
text: string;
26-
type: "text" | "speech";
27-
createdAt: number;
28-
translated?: string;
29-
romanization?: string;
30-
};
20+
import { FragmentComponent, type Fragment } from "./fragment";
3121

3222
const MainView: () => JSX.Element = () => {
3323
const [inputText, setInputText] = useState("");
@@ -302,48 +292,16 @@ const MainView: () => JSX.Element = () => {
302292
className="flex-1 overflow-y-auto px-0 min-h-0"
303293
>
304294
<div className="min-h-full flex flex-col justify-end">
305-
{inputLanguage === outputLanguage ? (
306-
<div className="p-4 flex flex-col-reverse gap-4">
307-
{[...fragments].reverse().map((fragment) => (
308-
<div
309-
key={fragment.id}
310-
className={`border border-black p-4 rounded-md ${
311-
fragment.type === "speech" ? "bg-black/5" : ""
312-
}`}
313-
>
314-
<div>{fragment.text}</div>
315-
</div>
316-
))}
317-
</div>
318-
) : (
319-
<div className="p-4 flex flex-col-reverse gap-4">
320-
{[...fragments].reverse().map((fragment) => (
321-
<div key={fragment.id} className="grid grid-cols-2 gap-4 items-start">
322-
<div
323-
className={`border border-black p-4 rounded-md ${
324-
fragment.type === "speech" ? "bg-black/5" : ""
325-
}`}
326-
>
327-
{fragment.text}
328-
</div>
329-
<div className="border border-black p-4 rounded-md">
330-
{fragment.translated ? (
331-
<>
332-
<div className="font-medium">{fragment.translated}</div>
333-
{outputLanguage === Language.CHINESE_CN && fragment.romanization && (
334-
<div className="text-xs mt-1 text-gray-500">
335-
{fragment.romanization}
336-
</div>
337-
)}
338-
</>
339-
) : (
340-
<Skeleton className="h-4 w-full" />
341-
)}
342-
</div>
343-
</div>
344-
))}
345-
</div>
346-
)}
295+
<div className="p-4 flex flex-col-reverse gap-4">
296+
{[...fragments].reverse().map((fragment) => (
297+
<FragmentComponent
298+
key={fragment.id}
299+
fragment={fragment}
300+
showTranslation={inputLanguage !== outputLanguage}
301+
outputLanguage={outputLanguage}
302+
/>
303+
))}
304+
</div>
347305
</div>
348306
</div>
349307
<div className="flex-none px-4 pt-2 pb-4 border-t">

0 commit comments

Comments
 (0)