streaming, markdown, model selecting, and lots more

This commit is contained in:
Zoe
2026-02-02 23:16:06 -06:00
parent 8c28946703
commit d5a5945c03
114 changed files with 6109 additions and 2938 deletions
+65 -53
View File
@@ -1,47 +1,46 @@
<script setup lang="ts">
import { authClient } from '~~/lib/auth-client';
import { deriveKey } from '~/utils/crypto';
definePageMeta({
layout: 'auth',
})
});
const { signIn, session, fetchSession, authClient } = useAuth();
if (session.value !== null) {
navigateTo("/");
}
const to = useRoute().query.to as string | undefined;
const form = reactive({
email: "",
password: "",
email: '',
password: '',
});
const loading = ref(false);
let emailInputEl = ref<HTMLInputElement | null>(null);
let passwordInputEl = ref<HTMLInputElement | null>(null);
const emailInputEl = ref<HTMLInputElement | null>(null);
const passwordInputEl = ref<HTMLInputElement | null>(null);
let tempForm = {
email: "",
password: "",
}
const tempForm = {
email: '',
password: '',
};
// prevent text fields from clearing on hydration
onBeforeMount(() => {
tempForm.email = (document.getElementById("email") as HTMLInputElement)?.value ?? "";
tempForm.password = (document.getElementById("password") as HTMLInputElement)?.value ?? "";
})
tempForm.email = (document.getElementById('email') as HTMLInputElement)?.value ?? '';
tempForm.password = (document.getElementById('password') as HTMLInputElement)?.value ?? '';
});
let hydrated = ref(false);
const hydrated = ref(false);
onMounted(() => {
form.email = tempForm.email;
form.password = tempForm.password;
hydrated.value = true;
emailInputEl.value!.addEventListener("input", () => {
emailInputEl.value!.setCustomValidity("");
emailInputEl.value!.addEventListener('input', () => {
emailInputEl.value!.setCustomValidity('');
});
passwordInputEl.value!.addEventListener("input", () => {
passwordInputEl.value!.setCustomValidity("");
passwordInputEl.value!.addEventListener('input', () => {
passwordInputEl.value!.setCustomValidity('');
});
});
@@ -57,40 +56,52 @@ const submit = async () => {
loading.value = true;
await signIn.email({
const { data, error } = await authClient.signIn.email({
email: form.email,
password: form.password,
}, {
onSuccess: async () => {
await fetchSession();
navigateTo("/")
},
onError: (ctx) => {
const error = ctx.error.code as keyof typeof authClient.$ERROR_CODES;
// TODO: i18n
// ref https://www.better-auth.com/docs/concepts/client#error-codes
switch (error) {
case "INVALID_PASSWORD":
passwordInputEl.value!.setCustomValidity(ctx.error.message);
passwordInputEl.value!.reportValidity();
break;
case "ACCOUNT_NOT_FOUND":
case "USER_NOT_FOUND":
case "USER_EMAIL_NOT_FOUND":
emailInputEl.value!.setCustomValidity(ctx.error.message);
emailInputEl.value!.reportValidity();
break;
default:
console.log(ctx.error);
alert(`Something went wrong. ${ctx.error.message}`);
break;
}
}
});
loading.value = false;
}
if (error) {
const errorCode = error.code! as keyof typeof authClient.$ERROR_CODES;
// TODO: i18n
// ref https://www.better-auth.com/docs/concepts/client#error-codes
switch (errorCode) {
case 'INVALID_PASSWORD':
passwordInputEl.value!.setCustomValidity(error.message!);
passwordInputEl.value!.reportValidity();
break;
case 'ACCOUNT_NOT_FOUND':
case 'USER_NOT_FOUND':
case 'USER_EMAIL_NOT_FOUND':
emailInputEl.value!.setCustomValidity(error.message!);
emailInputEl.value!.reportValidity();
break;
default:
console.log(error);
alert(`Something went wrong. ${error.message}`);
break;
}
return;
}
const key = await deriveKey(form.password, data.user.id);
localStorage.setItem('encryptionKey', JSON.stringify(key));
// force a session refetch
clearNuxtData();
// success
const triplit = useTriplitClient();
if ('startSession' in triplit) {
await triplit.startSession(data.token);
}
return navigateTo(to ?? '/');
};
</script>
<template>
@@ -103,9 +114,10 @@ const submit = async () => {
<input required minlength="8" maxlength="128" ref="passwordInputEl" type="password"
autocomplete="current-password" id="password" v-model="form.password" />
<button :disabled="!hydrated" class="accent" type="submit">
<Icon v-if="loading" width="24" name="svg-spinners:90-ring-with-bg" />
<Icon v-if="loading" class="text-6" name="svg-spinners:90-ring-with-bg" />
<span v-else>Login</span>
</button>
</form>
<p class="text-center">Dont have an account? <a href="/auth/register">Register</a></p>
<p class="text-center">Dont have an account? <a
:href="to ? `/auth/register?to=${to}` : '/auth/register'">Register</a></p>
</template>
+83 -68
View File
@@ -1,47 +1,51 @@
<script setup lang="ts">
import { authClient } from '~~/lib/auth-client';
import { deriveKey } from '~/utils/crypto';
definePageMeta({
layout: 'auth',
})
});
const { signUp, session, fetchSession, authClient } = useAuth();
const { session } = await useAuth();
const to = useRoute().query.to as string | undefined;
if (session.value !== null) {
navigateTo("/");
await navigateTo(to ?? '/');
}
if (import.meta.server) {
if (process.env.DISABLE_SIGNUP?.toLowerCase() === "true" || process.env.DISABLE_SIGNUP === "1") {
navigateTo("/auth/login")
if (process.env.DISABLE_SIGNUP?.toLowerCase() === 'true' || process.env.DISABLE_SIGNUP === '1') {
await navigateTo(to ? `/auth/login?to=${to}` : '/auth/login');
}
}
const form = reactive({
name: "",
email: "",
password: "",
confirmPassword: "",
name: '',
email: '',
password: '',
confirmPassword: '',
});
const loading = ref(false);
let nameInputEl = ref<HTMLInputElement | null>(null);
let emailInputEl = ref<HTMLInputElement | null>(null);
let passwordInputEl = ref<HTMLInputElement | null>(null);
let confirmPasswordInputEl = ref<HTMLInputElement | null>(null);
const nameInputEl = ref<HTMLInputElement | null>(null);
const emailInputEl = ref<HTMLInputElement | null>(null);
const passwordInputEl = ref<HTMLInputElement | null>(null);
const confirmPasswordInputEl = ref<HTMLInputElement | null>(null);
let tempForm = {
name: "",
email: "",
password: "",
confirmPassword: "",
}
const tempForm = {
name: '',
email: '',
password: '',
confirmPassword: '',
};
// prevent text fields from clearing on hydration
onBeforeMount(() => {
tempForm.name = (document.getElementById("name") as HTMLInputElement)?.value ?? "";
tempForm.email = (document.getElementById("email") as HTMLInputElement)?.value ?? "";
tempForm.password = (document.getElementById("password") as HTMLInputElement)?.value ?? "";
tempForm.confirmPassword = (document.getElementById("confirmPassword") as HTMLInputElement)?.value ?? "";
})
tempForm.name = (document.getElementById('name') as HTMLInputElement)?.value ?? '';
tempForm.email = (document.getElementById('email') as HTMLInputElement)?.value ?? '';
tempForm.password = (document.getElementById('password') as HTMLInputElement)?.value ?? '';
tempForm.confirmPassword = (document.getElementById('confirmPassword') as HTMLInputElement)?.value ?? '';
});
const hydrated = ref(false);
@@ -52,25 +56,24 @@ onMounted(() => {
form.confirmPassword = tempForm.confirmPassword;
hydrated.value = true;
nameInputEl.value!.addEventListener("input", () => {
nameInputEl.value!.setCustomValidity("");
nameInputEl.value!.addEventListener('input', () => {
nameInputEl.value!.setCustomValidity('');
});
emailInputEl.value!.addEventListener("input", () => {
emailInputEl.value!.setCustomValidity("");
emailInputEl.value!.addEventListener('input', () => {
emailInputEl.value!.setCustomValidity('');
});
passwordInputEl.value!.addEventListener("input", () => {
passwordInputEl.value!.setCustomValidity("");
passwordInputEl.value!.addEventListener('input', () => {
passwordInputEl.value!.setCustomValidity('');
});
confirmPasswordInputEl.value!.addEventListener("input", () => {
confirmPasswordInputEl.value!.addEventListener('input', () => {
if (passwordInputEl.value!.value !== confirmPasswordInputEl.value!.value) {
confirmPasswordInputEl.value!.setCustomValidity("Passwords do not match");
confirmPasswordInputEl.value!.setCustomValidity('Passwords do not match');
confirmPasswordInputEl.value!.reportValidity();
} else {
confirmPasswordInputEl.value!.setCustomValidity("");
confirmPasswordInputEl.value!.setCustomValidity('');
}
});
});
@@ -86,50 +89,62 @@ const submit = async () => {
}
if (form.password !== form.confirmPassword) {
alert("Passwords do not match")
alert('Passwords do not match');
return;
}
loading.value = true;
await signUp.email({
const { data, error } = await authClient.signUp.email({
name: form.name,
email: form.email,
password: form.password,
}, {
onSuccess: async () => {
await fetchSession();
navigateTo("/")
},
onError: (ctx) => {
const error = ctx.error.code as keyof typeof authClient.$ERROR_CODES;
// TODO: i18n
// ref https://www.better-auth.com/docs/concepts/client#error-codes
switch (error) {
case "USER_ALREADY_EXISTS":
case "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL":
emailInputEl.value!.setCustomValidity("Account with this email already exists");
emailInputEl.value!.reportValidity();
break;
case "INVALID_EMAIL":
emailInputEl.value!.setCustomValidity(ctx.error.message);
emailInputEl.value!.reportValidity();
break;
case "INVALID_PASSWORD":
passwordInputEl.value!.setCustomValidity(ctx.error.message);
passwordInputEl.value!.reportValidity();
break;
default:
console.log(ctx.error);
alert("Something went wrong")
break;
}
}
});
loading.value = false;
}
if (error) {
const errorCode = error.code! as keyof typeof authClient.$ERROR_CODES;
// TODO: i18n
// ref https://www.better-auth.com/docs/concepts/client#error-codes
switch (errorCode) {
case 'USER_ALREADY_EXISTS':
case 'USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL':
emailInputEl.value!.setCustomValidity('Account with this email already exists');
emailInputEl.value!.reportValidity();
break;
case 'INVALID_EMAIL':
emailInputEl.value!.setCustomValidity(error.message!);
emailInputEl.value!.reportValidity();
break;
case 'INVALID_PASSWORD':
passwordInputEl.value!.setCustomValidity(error.message!);
passwordInputEl.value!.reportValidity();
break;
default:
console.log(error);
alert('Something went wrong: ' + error.message);
break;
}
return;
}
const key = await deriveKey(form.password, data.user.id);
localStorage.setItem('encryptionKey', JSON.stringify(key));
// force a session refetch
clearNuxtData();
// success
const triplit = useTriplitClient();
if ('startSession' in triplit) {
await triplit.startSession(data.token!);
}
return navigateTo(to ?? '/');
};
</script>
<template>
@@ -151,5 +166,5 @@ const submit = async () => {
<span v-else>Register</span>
</button>
</form>
<p class="text-center">Already have an account? <a href="/auth/login">Login</a></p>
<p class="text-center">Already have an account? <a :href="to ? `/auth/login?to=${to}` : '/auth/login'">Login</a></p>
</template>