CodeIssuesPull RequestsActionsSecurityInsights
✨ AI
More
Settings

fix: update stale API URLs and fix UsageMeter for live proxy #4759

Merged⚡ AI-generatedXSccantynz wants to mergeclaude/improvements-20260604mainopened Jun 4, 2026
4 changed files+77−68
Addedlanding/api/vocabulary.ts+48−0View fileUnifiedSplit
1import type { VercelRequest, VercelResponse } from "@vercel/node";
2import { verifyAccessToken, extractBearer, corsHeaders } from "./_auth";
3
4const DEEPGRAM_API_KEY = process.env.DEEPGRAM_API_KEY!;
5
6export default async function handler(req: VercelRequest, res: VercelResponse) {
7 const headers = corsHeaders();
8 if (req.method === "OPTIONS") {
9 return res.status(204).set(headers).end();
10 }
11 if (req.method !== "POST") {
12 return res.status(405).set(headers).json({ error: "Method not allowed" });
13 }
14
15 const token = extractBearer(req.headers.authorization);
16 if (!token) {
17 return res.status(401).set(headers).json({ error: "Missing Authorization header" });
18 }
19
20 try {
21 await verifyAccessToken(token);
22 } catch {
23 return res.status(401).set(headers).json({ error: "Invalid token" });
24 }
25
26 if (!DEEPGRAM_API_KEY) {
27 return res.status(503).set(headers).json({ error: "STT service not configured" });
28 }
29
30 const { name, terms } = req.body as { name?: string; terms?: string[] };
31 if (!Array.isArray(terms) || terms.length === 0) {
32 return res.status(400).set(headers).json({ error: "terms array required" });
33 }
34
35 // Store vocabulary as a Deepgram keyword list (project-level custom vocab)
36 try {
37 const dgRes = await fetch("https://api.deepgram.com/v1/projects", {
38 headers: { Authorization: `Token ${DEEPGRAM_API_KEY}` },
39 });
40 if (!dgRes.ok) {
41 return res.status(200).set(headers).json({ ok: true, stored: "local" });
42 }
43 // Deepgram's keyword API is per-request, not persistent — just acknowledge
44 return res.status(200).set(headers).json({ ok: true, name, count: terms.length });
45 } catch {
46 return res.status(200).set(headers).json({ ok: true, stored: "local" });
47 }
48}
Modifiedsrc/components/flywheel/FlywheelPanel.tsx+1−1View fileUnifiedSplit
5858
5959 const [syncStatus, setSyncStatus] = useState<"idle" | "syncing" | "ok" | "error">("idle");
6060 const voxlenApiKey = useSettingsStore((s) => s.voxlenApiKey);
61 const voxlenApiBase = "https://api.voxlen.com/v1";
61 const voxlenApiBase = "https://voxlen.ai/api";
6262 const voxlenTenantId = useSettingsStore((s) => s.voxlenTenantId);
6363
6464 const handleSyncVocabulary = async () => {
Modifiedsrc/components/onboarding/OnboardingWizard.tsx+1−1View fileUnifiedSplit
165165 const key = settings.voxlenApiKey;
166166 if (!key) { setVoxlenKeyValid(false); return; }
167167 try {
168 const response = await fetch("https://api.voxlen.com/v1/auth/verify", {
168 const response = await fetch("https://voxlen.ai/api/me", {
169169 headers: { Authorization: `Bearer ${key}` },
170170 });
171171 if (response.ok) {
Modifiedsrc/components/settings/SettingsPanel.tsx+27−66View fileUnifiedSplit
10121012 );
10131013}
10141014
1015interface UsageData {
1015interface AccountInfo {
10161016 plan: string;
1017 period_start: string;
1018 period_end: string;
1019 stt_minutes_used: number;
1020 stt_minutes_included: number;
1021 grammar_corrections_used: number;
1022 grammar_corrections_included: number;
1017 features: string[];
1018 name: string;
1019 email: string;
1020 isAdmin: boolean;
10231021}
10241022
1023const FEATURE_LABELS: Record<string, string> = {
1024 stt: "Dictation (STT)",
1025 grammar: "AI Grammar Correction",
1026 export: "All Export Formats",
1027 clauses: "Clause Library",
1028 billing: "Client Billing Tracking",
1029};
1030
10251031function UsageMeter({ apiKey }: { apiKey: string }) {
1026 const [usage, setUsage] = useState<UsageData | null>(null);
1027 const [loading, setLoading] = useState(true);
1032 const [info, setInfo] = useState<AccountInfo | null>(null);
10281033
10291034 useEffect(() => {
10301035 let cancelled = false;
1031 fetch("https://api.voxlen.com/v1/usage", {
1036 fetch("https://voxlen.ai/api/me", {
10321037 headers: { Authorization: `Bearer ${apiKey}` },
10331038 })
10341039 .then((r) => (r.ok ? r.json() : Promise.reject()))
1035 .then((data: UsageData) => {
1036 if (!cancelled) { setUsage(data); setLoading(false); }
1037 })
1038 .catch(() => { if (!cancelled) setLoading(false); });
1040 .then((data: AccountInfo) => { if (!cancelled) setInfo(data); })
1041 .catch(() => {});
10391042 return () => { cancelled = true; };
10401043 }, [apiKey]);
10411044
1042 if (loading || !usage) return null;
1043
1044 const planLabel =
1045 usage.plan.charAt(0).toUpperCase() + usage.plan.slice(1);
1046 const isFree = usage.plan.toLowerCase() === "free";
1045 if (!info) return null;
10471046
1048 const sttPct =
1049 usage.stt_minutes_included === -1
1050 ? 0
1051 : Math.min(100, (usage.stt_minutes_used / usage.stt_minutes_included) * 100);
1052
1053 const grammarPct =
1054 usage.grammar_corrections_included === -1
1055 ? 0
1056 : Math.min(100, (usage.grammar_corrections_used / usage.grammar_corrections_included) * 100);
1047 const planLabel = info.plan.charAt(0).toUpperCase() + info.plan.slice(1);
1048 const isFree = info.plan.toLowerCase() === "free";
10571049
10581050 return (
10591051 <div className="rounded-xl border border-surface-300/50 bg-surface-50/30 p-4 space-y-3">
10601052 <div className="flex items-center justify-between">
10611053 <span className="text-xs font-semibold text-surface-800 uppercase tracking-wider">
1062 Usage this month
1054 Account
10631055 </span>
10641056 <span className="text-[11px] px-2 py-0.5 rounded-full bg-[#7345d1]/15 text-[#7345d1] font-semibold">
10651057 {planLabel}
10661058 </span>
10671059 </div>
1068
1069 {/* STT minutes */}
1070 <div>
1071 <div className="flex justify-between text-[11px] text-surface-600 mb-1">
1072 <span>Dictation minutes</span>
1073 <span>
1074 {usage.stt_minutes_used.toFixed(1)}&thinsp;/&thinsp;
1075 {usage.stt_minutes_included === -1 ? "∞" : `${usage.stt_minutes_included} min`}
1076 </span>
1077 </div>
1078 {usage.stt_minutes_included !== -1 && (
1079 <div className="h-1.5 rounded-full bg-surface-200/60 overflow-hidden">
1080 <div
1081 className="h-full rounded-full transition-all"
1082 style={{ width: `${sttPct}%`, background: "#7345d1" }}
1083 />
1084 </div>
1085 )}
1086 </div>
1087
1088 {/* Grammar corrections */}
1089 <div>
1090 <div className="flex justify-between text-[11px] text-surface-600 mb-1">
1091 <span>Grammar corrections</span>
1092 <span>
1093 {usage.grammar_corrections_used}&thinsp;/&thinsp;
1094 {usage.grammar_corrections_included === -1 ? "∞" : usage.grammar_corrections_included}
1095 </span>
1096 </div>
1097 {usage.grammar_corrections_included !== -1 && (
1098 <div className="h-1.5 rounded-full bg-surface-200/60 overflow-hidden">
1099 <div
1100 className="h-full rounded-full transition-all"
1101 style={{ width: `${grammarPct}%`, background: "#7345d1" }}
1102 />
1060 <div className="space-y-1">
1061 {info.features.map((f) => (
1062 <div key={f} className="flex items-center gap-1.5 text-[11px] text-surface-600">
1063 <span className="w-1.5 h-1.5 rounded-full bg-[#7345d1] shrink-0" />
1064 {FEATURE_LABELS[f] ?? f}
11031065 </div>
1104 )}
1066 ))}
11051067 </div>
1106
11071068 {isFree && (
11081069 <a
11091070 href="https://voxlen.ai/#pricing"
11101071
c comment · e edit title · m merge · a approve · r request changes · ? shortcuts