feat: analytics dashboard + landing page marketing overhaul + SEO #4771
2 changed files+212−137
Modifiedlanding/src/App.tsx+40−11View fileUnifiedSplit
@@ -759,6 +759,7 @@ function Pricing() {
759759 cta: "Download Free",
760760 ctaStyle: "secondary",
761761 highlight: false,
762 priceId: null as string | null,
762763 },
763764 {
764765 name: "Pro",
@@ -780,6 +781,7 @@ function Pricing() {
780781 ctaStyle: "primary",
781782 highlight: true,
782783 badge: "Most Popular",
784 priceId: PRICE_PRO_MONTHLY,
783785 },
784786 {
785787 name: "Professional",
@@ -799,6 +801,7 @@ function Pricing() {
799801 ctaStyle: "secondary",
800802 highlight: false,
801803 badge: "Recommended for Pros",
804 priceId: PRICE_PROFESSIONAL_MONTHLY,
802805 },
803806 {
804807 name: "Lifetime",
@@ -815,6 +818,7 @@ function Pricing() {
815818 cta: "Get Lifetime",
816819 ctaStyle: "secondary",
817820 highlight: false,
821 priceId: PRICE_LIFETIME,
818822 },
819823 ];
820824
@@ -889,21 +893,46 @@ function Pricing() {
889893 </li>
890894 ))}
891895 </ul>
892 <a
893 href="#download"
894 className={`block text-center h-11 leading-[44px] rounded-xl text-sm font-semibold transition-colors ${
895 t.ctaStyle === "primary"
896 ? "bg-marcoreid-600 text-white hover:bg-marcoreid-700 shadow-lg shadow-marcoreid-600/25"
897 : "bg-white/5 border border-white/10 text-white hover:bg-white/10"
898 }`}
899 >
900 {t.cta}
901 </a>
896 {t.priceId ? (
897 <button
898 type="button"
899 onClick={() => redirectToCheckout(t.priceId!)}
900 className={`w-full h-11 rounded-xl text-sm font-semibold transition-colors cursor-pointer ${
901 t.ctaStyle === "primary"
902 ? "bg-marcoreid-600 text-white hover:bg-marcoreid-700 shadow-lg shadow-marcoreid-600/25"
903 : "bg-white/5 border border-white/10 text-white hover:bg-white/10"
904 }`}
905 >
906 {t.cta}
907 </button>
908 ) : (
909 <a
910 href="#download"
911 className={`block text-center h-11 leading-[44px] rounded-xl text-sm font-semibold transition-colors ${
912 t.ctaStyle === "primary"
913 ? "bg-marcoreid-600 text-white hover:bg-marcoreid-700 shadow-lg shadow-marcoreid-600/25"
914 : "bg-white/5 border border-white/10 text-white hover:bg-white/10"
915 }`}
916 >
917 {t.cta}
918 </a>
919 )}
902920 </motion.div>
903921 ))}
904922 </motion.div>
905923
906 <p className="text-center text-xs text-zinc-600 mt-10 max-w-2xl mx-auto leading-relaxed">
924 {/* Stripe trust badge */}
925 <motion.div
926 initial={{ opacity: 0 }}
927 whileInView={{ opacity: 1 }}
928 viewport={{ once: true }}
929 className="flex items-center justify-center gap-2 mt-8 text-xs text-zinc-500"
930 >
931 <Lock className="h-3.5 w-3.5 text-zinc-600" />
932 <span>Secure payment by <span className="text-zinc-400 font-medium">Stripe</span> — your card details never touch our servers</span>
933 </motion.div>
934
935 <p className="text-center text-xs text-zinc-600 mt-4 max-w-2xl mx-auto leading-relaxed">
907936 Pro and Professional include a 14-day free trial. No credit card required. Cancel anytime.
908937 All AI costs (speech-to-text and grammar correction) are included — your subscription
909938 covers everything. Audio still streams directly from your device to AI providers and is
Modifiedsrc/components/settings/SettingsPanel.tsx+172−126View fileUnifiedSplit
@@ -966,21 +966,45 @@ function AdvancedSettings() {
966966
967967function VoxlenApiSettings() {
968968 const settings = useSettingsStore();
969 const [testStatus, setTestStatus] = useState<"idle" | "testing" | "ok" | "error">("idle");
969 const [verifying, setVerifying] = useState(false);
970 const [keyInput, setKeyInput] = useState("");
971 const [keyError, setKeyError] = useState("");
972 const isConnected = Boolean(settings.voxlenApiKey);
970973
971 const testConnection = async () => {
972 if (!settings.voxlenApiKey) return;
973 setTestStatus("testing");
974 const openSignUp = async () => {
974975 try {
975 const res = await fetch(
976 `${settings.voxlenTenantId ? "https://api.voxlen.com/v1" : "https://api.voxlen.com/v1"}/health`,
977 { headers: { Authorization: `Bearer ${settings.voxlenApiKey}` } }
978 );
979 setTestStatus(res.ok ? "ok" : "error");
976 const { invoke } = await import("@tauri-apps/api/core");
977 await invoke("open_url", { url: "https://voxlen.ai/#download" });
980978 } catch {
981 setTestStatus("error");
979 window.open("https://voxlen.ai/#download", "_blank");
982980 }
983 setTimeout(() => setTestStatus("idle"), 3000);
981 };
982
983 const connect = async () => {
984 const key = keyInput.trim();
985 if (!key) return;
986 setVerifying(true);
987 setKeyError("");
988 try {
989 const res = await fetch("https://api.voxlen.com/v1/health", {
990 headers: { Authorization: `Bearer ${key}` },
991 });
992 if (res.ok) {
993 settings.updateSetting("voxlenApiKey", key);
994 setKeyInput("");
995 } else {
996 setKeyError("Invalid API key. Check your Voxlen dashboard.");
997 }
998 } catch {
999 // Network unreachable — accept key anyway (API may not be live yet)
1000 settings.updateSetting("voxlenApiKey", key);
1001 setKeyInput("");
1002 }
1003 setVerifying(false);
1004 };
1005
1006 const disconnect = () => {
1007 settings.updateSetting("voxlenApiKey", "");
9841008 };
9851009
9861010 const CONTEXT_OPTIONS = [
@@ -1000,131 +1024,153 @@ function VoxlenApiSettings() {
10001024 ];
10011025
10021026 return (
1003 <div className="space-y-6 max-w-lg">
1004 <SectionHeader
1005 title="Voxlen API"
1006 description="Connect to api.voxlen.com for enhanced transcription with domain-specific formatting. Your API key stays on this device."
1007 />
1027 <div className="space-y-5 max-w-lg">
1028 {/* Connected state */}
1029 {isConnected ? (
1030 <div className="rounded-xl border border-emerald-500/30 bg-emerald-500/5 p-5">
1031 <div className="flex items-center gap-3 mb-4">
1032 <div className="w-9 h-9 rounded-full bg-emerald-500/15 flex items-center justify-center">
1033 <span className="text-emerald-400 text-lg">✓</span>
1034 </div>
1035 <div>
1036 <p className="text-sm font-semibold text-surface-900">Voxlen Account Connected</p>
1037 <p className="text-[11px] text-surface-600">
1038 Transcription and grammar AI powered by Voxlen. No provider keys needed.
1039 </p>
1040 </div>
1041 </div>
1042 <button
1043 onClick={disconnect}
1044 className="text-[11px] text-surface-500 hover:text-red-400 transition-colors"
1045 >
1046 Disconnect account
1047 </button>
1048 </div>
1049 ) : (
1050 /* Not connected */
1051 <div className="rounded-xl border border-surface-300/60 bg-surface-50/40 p-5 space-y-4">
1052 <div className="text-center pb-2">
1053 <div className="flex items-baseline justify-center gap-0.5 mb-1">
1054 <span className="font-display text-xl text-surface-900 tracking-tight-display">Vox</span>
1055 <span className="font-display text-xl italic text-brass-500 tracking-tight-display">len</span>
1056 </div>
1057 <p className="text-[12px] text-surface-600 max-w-xs mx-auto">
1058 Connect your Voxlen account and the app works immediately — no Deepgram or Anthropic keys required.
1059 </p>
1060 </div>
10081061
1009 <SettingRow>
1010 <div className="space-y-2">
1011 <Input
1012 label="Voxlen API Key"
1013 type="password"
1014 value={settings.voxlenApiKey}
1015 onChange={(e) => settings.updateSetting("voxlenApiKey", e.target.value)}
1016 placeholder="vx-..."
1017 />
1018 <div className="flex items-center gap-2 pt-1">
1062 <button
1063 onClick={openSignUp}
1064 className="w-full flex items-center justify-center gap-2 bg-[#7345d1] hover:bg-[#5c35b0] text-white font-semibold text-sm py-2.5 rounded-lg transition-colors"
1065 >
1066 Get your API key at voxlen.ai
1067 </button>
1068
1069 <div className="relative flex items-center gap-3">
1070 <div className="flex-1 h-px bg-surface-300/50" />
1071 <span className="text-[10px] text-surface-500 uppercase tracking-wider">already have a key?</span>
1072 <div className="flex-1 h-px bg-surface-300/50" />
1073 </div>
1074
1075 <div className="space-y-2">
1076 <input
1077 type="password"
1078 value={keyInput}
1079 onChange={(e) => { setKeyInput(e.target.value); setKeyError(""); }}
1080 onKeyDown={(e) => e.key === "Enter" && connect()}
1081 placeholder="vx-..."
1082 className="w-full bg-surface-50 border border-surface-300/70 rounded-lg px-3 py-2 text-sm text-surface-900 placeholder-surface-500 focus:outline-none focus:border-[#7345d1] shadow-inset-hairline"
1083 />
1084 {keyError && <p className="text-[11px] text-red-500">{keyError}</p>}
10191085 <Button
1020 size="sm"
10211086 variant="secondary"
1022 onClick={testConnection}
1023 disabled={!settings.voxlenApiKey || testStatus === "testing"}
1087 size="sm"
1088 onClick={connect}
1089 disabled={!keyInput.trim() || verifying}
1090 className="w-full"
10241091 >
1025 {testStatus === "testing" ? "Testing…" : "Test Connection"}
1092 {verifying ? "Connecting…" : "Connect"}
10261093 </Button>
1027 {testStatus === "ok" && (
1028 <span className="text-[11px] text-emerald-500 font-medium">Connected</span>
1029 )}
1030 {testStatus === "error" && (
1031 <span className="text-[11px] text-red-500 font-medium">Connection failed</span>
1032 )}
10331094 </div>
10341095 </div>
1035 </SettingRow>
1036
1037 <SettingRow>
1038 <Input
1039 label="Tenant ID"
1040 value={settings.voxlenTenantId}
1041 onChange={(e) => settings.updateSetting("voxlenTenantId", e.target.value)}
1042 placeholder="Your firm or organisation ID"
1043 />
1044 </SettingRow>
1045
1046 <SettingRow>
1047 <Select
1048 label="Dictation Context"
1049 value={settings.voxlenContext}
1050 onChange={(v) => settings.updateSetting("voxlenContext", v)}
1051 options={CONTEXT_OPTIONS}
1052 />
1053 </SettingRow>
1054
1055 <SettingRow>
1056 <Switch
1057 label="Privileged Mode"
1058 description="Block all cloud STT — local processing only. For highly sensitive matters where audio must never leave the device."
1059 checked={settings.privilegedMode}
1060 onChange={(v) => settings.updateSetting("privilegedMode", v)}
1061 />
1062 </SettingRow>
1063
1064 <SettingRow>
1065 <Switch
1066 label="Legal Mode"
1067 description="Enable Latin phrase recognition, legal currency formatting, and jurisdiction-specific smart format."
1068 checked={settings.legalMode}
1069 onChange={(v) => settings.updateSetting("legalMode", v)}
1070 />
1071 </SettingRow>
1072
1073 {settings.legalMode && (
1074 <SettingRow>
1075 <Select
1076 label="Jurisdiction"
1077 value={settings.jurisdiction}
1078 onChange={(v) =>
1079 settings.updateSetting("jurisdiction", v as typeof settings.jurisdiction)
1080 }
1081 options={[
1082 { value: "global", label: "Global", description: "International / neutral" },
1083 { value: "uk", label: "United Kingdom", description: "England & Wales, Scotland" },
1084 { value: "us", label: "United States", description: "Federal + state law" },
1085 { value: "australia", label: "Australia", description: "Commonwealth + state law" },
1086 { value: "nz", label: "New Zealand", description: "NZ common law" },
1087 { value: "canada", label: "Canada", description: "Federal + provincial law" },
1088 ]}
1089 />
1090 </SettingRow>
10911096 )}
10921097
1093 {settings.legalMode && (
1094 <SettingRow>
1095 <Slider
1096 label="Default Billable Rate"
1097 value={settings.billableRatePerHour}
1098 onChange={(v) => settings.updateSetting("billableRatePerHour", v)}
1099 min={0}
1100 max={2000}
1101 step={25}
1102 formatValue={(v) => `$${v}/hr`}
1103 />
1104 </SettingRow>
1098 {/* Settings shown when connected */}
1099 {isConnected && (
1100 <>
1101 <SectionHeader title="Dictation Settings" description="" />
1102
1103 <SettingRow>
1104 <Select
1105 label="Dictation Context"
1106 value={settings.voxlenContext}
1107 onChange={(v) => settings.updateSetting("voxlenContext", v)}
1108 options={CONTEXT_OPTIONS}
1109 />
1110 </SettingRow>
1111
1112 <SettingRow>
1113 <Switch
1114 label="Privileged Mode"
1115 description="Block all cloud STT — local processing only. For highly sensitive matters where audio must never leave the device."
1116 checked={settings.privilegedMode}
1117 onChange={(v) => settings.updateSetting("privilegedMode", v)}
1118 />
1119 </SettingRow>
1120
1121 <SettingRow>
1122 <Switch
1123 label="Legal Mode"
1124 description="Enable Latin phrase recognition, legal currency formatting, and jurisdiction-specific smart format."
1125 checked={settings.legalMode}
1126 onChange={(v) => settings.updateSetting("legalMode", v)}
1127 />
1128 </SettingRow>
1129
1130 {settings.legalMode && (
1131 <SettingRow>
1132 <Select
1133 label="Jurisdiction"
1134 value={settings.jurisdiction}
1135 onChange={(v) =>
1136 settings.updateSetting("jurisdiction", v as typeof settings.jurisdiction)
1137 }
1138 options={[
1139 { value: "global", label: "Global", description: "International / neutral" },
1140 { value: "uk", label: "United Kingdom", description: "England & Wales, Scotland" },
1141 { value: "us", label: "United States", description: "Federal + state law" },
1142 { value: "australia", label: "Australia", description: "Commonwealth + state law" },
1143 { value: "nz", label: "New Zealand", description: "NZ common law" },
1144 { value: "canada", label: "Canada", description: "Federal + provincial law" },
1145 ]}
1146 />
1147 </SettingRow>
1148 )}
1149
1150 {settings.legalMode && (
1151 <SettingRow>
1152 <Slider
1153 label="Default Billable Rate"
1154 value={settings.billableRatePerHour}
1155 onChange={(v) => settings.updateSetting("billableRatePerHour", v)}
1156 min={0}
1157 max={2000}
1158 step={25}
1159 formatValue={(v) => `$${v}/hr`}
1160 />
1161 </SettingRow>
1162 )}
1163
1164 <SettingRow>
1165 <Input
1166 label="Firm / Organisation ID (optional)"
1167 value={settings.voxlenTenantId}
1168 onChange={(e) => settings.updateSetting("voxlenTenantId", e.target.value)}
1169 placeholder="For team / SSO accounts"
1170 />
1171 </SettingRow>
1172 </>
11051173 )}
1106
1107 <div className="rounded-md bg-surface-50/60 border border-surface-300/50 shadow-inset-hairline p-4 mt-2">
1108 <h4 className="label-caps mb-3 block">How It Works</h4>
1109 <ul className="space-y-1.5 text-[12px] text-surface-700 leading-relaxed">
1110 <li className="flex items-start gap-2">
1111 <span className="text-brass-500 mt-0.5">—</span>
1112 Audio is streamed to api.voxlen.com for transcription with domain-aware formatting.
1113 </li>
1114 <li className="flex items-start gap-2">
1115 <span className="text-brass-500 mt-0.5">—</span>
1116 Context tells the server how to format output (contracts, depositions, tax memos, etc.).
1117 </li>
1118 <li className="flex items-start gap-2">
1119 <span className="text-brass-500 mt-0.5">—</span>
1120 Falls back to Deepgram or Whisper if Voxlen API is not configured.
1121 </li>
1122 <li className="flex items-start gap-2">
1123 <span className="text-brass-500 mt-0.5">—</span>
1124 Your API key is stored locally — never transmitted beyond api.voxlen.com.
1125 </li>
1126 </ul>
1127 </div>
11281174 </div>
11291175 );
11301176}
11311177
c comment · e edit title · m merge · a approve · r request changes · ? shortcuts