initial commit
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
<script setup lang="ts">
|
||||
import type { Message } from '~~/types'
|
||||
|
||||
const { createTopic, activeTopic } = await useTopics()
|
||||
const { activeAgent } = await useAgents()
|
||||
const loading = ref(false)
|
||||
const messages = useState<Message[]>('messages', () => [])
|
||||
const generatingMessage = ref('')
|
||||
|
||||
if (activeTopic.value !== undefined) {
|
||||
const topicData = await useFetch(`/api/topics/${activeTopic.value.id}`)
|
||||
if (topicData.error.value) throw topicData.error
|
||||
messages.value = topicData.data.value!.messages
|
||||
console.log(messages.value)
|
||||
}
|
||||
|
||||
const handleSubmit = async (message: string) => {
|
||||
loading.value = true
|
||||
let topic;
|
||||
if (activeTopic.value) {
|
||||
topic = activeTopic.value
|
||||
} else {
|
||||
topic = await createTopic('New Topic', activeAgent.value!.id)
|
||||
}
|
||||
|
||||
console.log(topic.id)
|
||||
|
||||
const generation = await $fetch(`/api/chat/generate`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
topicId: topic.id,
|
||||
messages: [{
|
||||
type: 'user',
|
||||
message
|
||||
}]
|
||||
})
|
||||
})
|
||||
|
||||
const response = await $fetch<ReadableStream>(`/api/chat/stream/${generation.generationId}`, {
|
||||
method: 'get',
|
||||
responseType: 'stream',
|
||||
})
|
||||
|
||||
// Create a new ReadableStream from the response with TextDecoderStream to get the data as text
|
||||
const reader = response.pipeThrough(new TextDecoderStream()).getReader()
|
||||
|
||||
generatingMessage.value = ''
|
||||
|
||||
// Read the data from the stream and update the UI
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) {
|
||||
break
|
||||
}
|
||||
|
||||
const { type, data } = JSON.parse(value)
|
||||
|
||||
if (type === 'token') {
|
||||
generatingMessage.value += data
|
||||
continue
|
||||
}
|
||||
|
||||
if (type === 'complete') {
|
||||
if (generatingMessage.value !== data) {
|
||||
generatingMessage.value = data
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
loading.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full w-full pb-4 justify-center">
|
||||
<div class="max-w-4xl h-full w-full flex flex-col">
|
||||
<!-- chat pane -->
|
||||
<div class="flex h-full flex-col gap-6">
|
||||
<p v-if="activeTopic !== undefined" v-for="message in messages" :key="message.id">
|
||||
{{ message.content }}
|
||||
</p>
|
||||
<p v-else>No messages yet</p>
|
||||
<p v-if="generatingMessage" class="text-sm text-gray-500">{{ generatingMessage }}</p>
|
||||
</div>
|
||||
|
||||
<ChatInput @submit="handleSubmit" :loading="loading" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
const { activeAgent: agent, updateAgent } = await useAgents();
|
||||
const route = useRoute()
|
||||
|
||||
if (agent.value === undefined) navigateTo('/');
|
||||
|
||||
const handleInput = (e: Event) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
if (target.value.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateAgent(agent.value!.id, { name: target.value });
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-4 px-14">
|
||||
<div class="flex items-center gap-4">
|
||||
<div>
|
||||
<img v-if="agent?.imageUrl" :src="agent.imageUrl" class="w-16 h-16 rounded-full object-cover" />
|
||||
<Icon v-else name="mynaui:check-hexagon" class="text-16" />
|
||||
</div>
|
||||
<input @input="handleInput" placeholder="Agent Name..."
|
||||
class="placeholder:text-[var(--color-highlight)] w-full bg-transparent rounded-none border-b-4 border-b-[var(--color-highlight-high)] text-12 p-0"
|
||||
type="text" :value="agent?.name" />
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm text-[var(--color-subtle)]">Agent ID:</span>
|
||||
<span class="text-sm font-semibold">{{ route.params.id }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,96 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
layout: 'auth',
|
||||
})
|
||||
|
||||
const { signIn, session, fetchSession, authClient } = useAuth();
|
||||
|
||||
if (session.value !== null) {
|
||||
navigateTo("/");
|
||||
}
|
||||
|
||||
const form = reactive({
|
||||
name: "",
|
||||
email: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
});
|
||||
const loading = ref(false);
|
||||
|
||||
let emailInputEl = ref<HTMLInputElement | null>(null);
|
||||
let passwordInputEl = ref<HTMLInputElement | null>(null);
|
||||
|
||||
onMounted(() => {
|
||||
emailInputEl.value!.addEventListener("input", () => {
|
||||
emailInputEl.value!.setCustomValidity("");
|
||||
});
|
||||
|
||||
passwordInputEl.value!.addEventListener("input", () => {
|
||||
passwordInputEl.value!.setCustomValidity("");
|
||||
});
|
||||
});
|
||||
|
||||
const submit = async () => {
|
||||
const inputs = [emailInputEl, passwordInputEl];
|
||||
|
||||
for (const input of inputs) {
|
||||
if (input.value?.validity.valid === false) {
|
||||
input.value!.reportValidity();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
|
||||
await 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;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1 class="font-bold text-center mb-4">Login</h1>
|
||||
<form class="flex flex-col [&>input]:mb-2" @submit.prevent="submit">
|
||||
<label for="email">Email</label>
|
||||
<input required pattern=".{1,}@.{1,}\..{2,3}" ref="emailInputEl" type="email" autocomplete="email" id="email"
|
||||
v-model="form.email" />
|
||||
<label for="password">Password</label>
|
||||
<input required minlength="8" maxlength="128" ref="passwordInputEl" type="password"
|
||||
autocomplete="current-password" id="password" v-model="form.password" />
|
||||
<button class="accent" type="submit">
|
||||
<iconify-icons v-if="loading" width="24" icon="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>
|
||||
</template>
|
||||
@@ -0,0 +1,132 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
layout: 'auth',
|
||||
})
|
||||
|
||||
const { signUp, session, fetchSession, authClient } = useAuth();
|
||||
|
||||
if (session.value !== null) {
|
||||
navigateTo("/");
|
||||
}
|
||||
|
||||
if (import.meta.server) {
|
||||
if (process.env.DISABLE_SIGNUP?.toLowerCase() === "true" || process.env.DISABLE_SIGNUP === "1") {
|
||||
navigateTo("/auth/login")
|
||||
}
|
||||
}
|
||||
|
||||
const form = reactive({
|
||||
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);
|
||||
|
||||
onMounted(() => {
|
||||
nameInputEl.value!.addEventListener("input", () => {
|
||||
nameInputEl.value!.setCustomValidity("");
|
||||
});
|
||||
|
||||
emailInputEl.value!.addEventListener("input", () => {
|
||||
emailInputEl.value!.setCustomValidity("");
|
||||
});
|
||||
|
||||
passwordInputEl.value!.addEventListener("input", () => {
|
||||
passwordInputEl.value!.setCustomValidity("");
|
||||
});
|
||||
|
||||
|
||||
confirmPasswordInputEl.value!.addEventListener("input", () => {
|
||||
if (passwordInputEl.value!.value !== confirmPasswordInputEl.value!.value) {
|
||||
confirmPasswordInputEl.value!.setCustomValidity("Passwords do not match");
|
||||
confirmPasswordInputEl.value!.reportValidity();
|
||||
} else {
|
||||
confirmPasswordInputEl.value!.setCustomValidity("");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const submit = async () => {
|
||||
const inputs = [nameInputEl, emailInputEl, passwordInputEl, confirmPasswordInputEl];
|
||||
|
||||
for (const input of inputs) {
|
||||
if (input.value?.validity.valid === false) {
|
||||
input.value!.reportValidity();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (form.password !== form.confirmPassword) {
|
||||
alert("Passwords do not match")
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
|
||||
await 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;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1 class="font-bold text-center mb-4">Register</h1>
|
||||
<form class="flex flex-col [&>input]:mb-2" @submit.prevent="submit">
|
||||
<label for="name">Name</label>
|
||||
<input required ref="nameInputEl" type="text" id="name" v-model="form.name" />
|
||||
<label for="email">Email</label>
|
||||
<input required pattern=".{1,}@.{1,}\..{2,3}" ref="emailInputEl" type="email" autocomplete="email" id="email"
|
||||
v-model="form.email" />
|
||||
<label for="password">Password</label>
|
||||
<input required minlength="8" maxlength="128" ref="passwordInputEl" type="password" autocomplete="new-password"
|
||||
id="password" v-model="form.password" />
|
||||
<label for="confirmPassword">Confirm Password</label>
|
||||
<input required minlength="8" maxlength="128" ref="confirmPasswordInputEl" type="password"
|
||||
autocomplete="new-password" id="confirmPassword" v-model="form.confirmPassword" />
|
||||
<button class="accent" type="submit">
|
||||
<Icon v-if="loading" class="text-6" name="svg-spinners:90-ring-with-bg" />
|
||||
<span v-else>Register</span>
|
||||
</button>
|
||||
</form>
|
||||
<p class="text-center">Already have an account? <a href="/auth/login">Login</a></p>
|
||||
</template>
|
||||
@@ -0,0 +1,101 @@
|
||||
<script setup lang="ts">
|
||||
const { user } = useAuth();
|
||||
|
||||
if (user.value === null) {
|
||||
navigateTo("/auth/login");
|
||||
}
|
||||
|
||||
const taglines = {
|
||||
morning: [
|
||||
"crush your goals before breakfast",
|
||||
"turn your productivity to 11",
|
||||
"start your day like a boss",
|
||||
"make it happen before noon",
|
||||
"rise and grind, repeat",
|
||||
"morning MVP, all day",
|
||||
"fuel your fire early",
|
||||
"own the first half of your day"
|
||||
],
|
||||
afternoon: [
|
||||
"afternoon focus session",
|
||||
"making afternoon moves",
|
||||
"steady progress continues",
|
||||
"keeping the flow going",
|
||||
"afternoon productivity boost",
|
||||
"momentum is building",
|
||||
"making it happen today",
|
||||
"afternoon hustle mode"
|
||||
],
|
||||
evening: [
|
||||
"finish strong today",
|
||||
"end on a high note",
|
||||
"wrap it up like a pro",
|
||||
"leave it all on the field",
|
||||
"tomorrow's success starts tonight",
|
||||
"last call for wins",
|
||||
"close it out like a champion",
|
||||
"seal the deal before bed"
|
||||
]
|
||||
}
|
||||
|
||||
const animatedText = ref("");
|
||||
const currentTaglineIndex = ref(0);
|
||||
const isDeleting = ref(false);
|
||||
|
||||
const typeWriter = (time: 'morning' | 'afternoon' | 'evening') => {
|
||||
const currentTaglines = taglines[time];
|
||||
const currentText = currentTaglines[currentTaglineIndex.value]!;
|
||||
|
||||
if (!isDeleting.value) {
|
||||
animatedText.value = currentText.substring(0, animatedText.value.length + 1);
|
||||
if (animatedText.value === currentText) {
|
||||
isDeleting.value = true;
|
||||
setTimeout(() => typeWriter(time), 2000);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
animatedText.value = currentText.substring(0, animatedText.value.length - 1);
|
||||
if (animatedText.value === "") {
|
||||
isDeleting.value = false;
|
||||
currentTaglineIndex.value = (currentTaglineIndex.value + 1) % currentTaglines.length;
|
||||
}
|
||||
}
|
||||
|
||||
const baseDeleteSpeed = 80;
|
||||
const speed = isDeleting.value
|
||||
? baseDeleteSpeed * (0.5 + 0.25 * (animatedText.value.length / currentText.length))
|
||||
: 100;
|
||||
|
||||
setTimeout(() => typeWriter(time), speed);
|
||||
};
|
||||
|
||||
const handleChatSubmit = (message: string) => {
|
||||
console.log('Message submitted:', message);
|
||||
// TODO: Implement chat functionality
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
let time: 'morning' | 'afternoon' | 'evening' = "morning";
|
||||
const now = new Date();
|
||||
const hours = now.getHours();
|
||||
|
||||
if (hours < 12) {
|
||||
time = "morning";
|
||||
} else if (hours < 22) {
|
||||
time = "afternoon";
|
||||
} else {
|
||||
time = "evening";
|
||||
}
|
||||
|
||||
// randomly select a tagline
|
||||
currentTaglineIndex.value = Math.floor(Math.random() * taglines[time].length);
|
||||
typeWriter(time);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col items-center pt-12 px-4 h-full gap-12">
|
||||
<h1 class="text-center">{{ animatedText }}<span class="cursor"> </span></h1>
|
||||
<ChatInput @submit="handleChatSubmit" />
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user