initial commit
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
import { createAuthClient } from 'better-auth/client'
|
||||
import type {
|
||||
InferSessionFromClient,
|
||||
InferUserFromClient,
|
||||
BetterAuthClientOptions,
|
||||
} from 'better-auth/client'
|
||||
import type { RouteLocationRaw } from 'vue-router'
|
||||
|
||||
|
||||
export function useAuth() {
|
||||
const url = useRequestURL()
|
||||
const headers = import.meta.server ? useRequestHeaders() : undefined
|
||||
|
||||
const authClient = createAuthClient({
|
||||
baseURL: url.origin,
|
||||
fetchOptions: {
|
||||
headers,
|
||||
},
|
||||
})
|
||||
|
||||
const session = useState<InferSessionFromClient<BetterAuthClientOptions> | null>('auth:session', () => null)
|
||||
const user = useState<InferUserFromClient<BetterAuthClientOptions> | null>('auth:user', () => null)
|
||||
const pending = import.meta.server ? ref(false) : useState('auth:sessionFetching', () => false)
|
||||
|
||||
const fetchSession = async () => {
|
||||
if (pending.value) {
|
||||
console.log('already fetching session')
|
||||
return
|
||||
}
|
||||
|
||||
pending.value = true
|
||||
const { data } = await authClient.getSession({
|
||||
fetchOptions: {
|
||||
headers,
|
||||
},
|
||||
})
|
||||
|
||||
session.value = data?.session || null
|
||||
user.value = data?.user || null
|
||||
pending.value = false
|
||||
return data
|
||||
}
|
||||
|
||||
if (import.meta.client) {
|
||||
authClient.$store.listen('$sessionSignal', async (signal) => {
|
||||
if (!signal) return
|
||||
await fetchSession()
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
session,
|
||||
user,
|
||||
pending,
|
||||
loggedIn: computed(() => !!session.value),
|
||||
signIn: authClient.signIn,
|
||||
signUp: authClient.signUp,
|
||||
async signOut() {
|
||||
const res = await authClient.signOut()
|
||||
session.value = null
|
||||
user.value = null
|
||||
await navigateTo('/auth/login')
|
||||
return res
|
||||
},
|
||||
fetchSession,
|
||||
authClient,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { ref } from 'vue'
|
||||
import type { Agent } from '~~/types'
|
||||
|
||||
export const useAgents = async () => {
|
||||
const { addTask, completeTask } = useTasks()
|
||||
|
||||
const fetchingAgents = ref(false);
|
||||
const agents: Ref<Agent[] | null> = useState('agents', () => null);
|
||||
const activeAgent = computed(() => {
|
||||
if (agents.value === null) return;
|
||||
|
||||
const routeId = useRoute().params.id;
|
||||
if (routeId === undefined) return;
|
||||
|
||||
const agent = agents.value.find(agent => agent.id === routeId);
|
||||
if (agent === undefined) return;
|
||||
|
||||
return agent;
|
||||
});
|
||||
|
||||
const refreshAgents = async () => {
|
||||
if (fetchingAgents.value) return;
|
||||
fetchingAgents.value = true;
|
||||
|
||||
const { data, error } = await useFetch('/api/agents');
|
||||
if (error.value) throw error;
|
||||
agents.value = data.value!;
|
||||
|
||||
fetchingAgents.value = false;
|
||||
}
|
||||
|
||||
if (agents.value === null) await refreshAgents();
|
||||
|
||||
const createAgent = async () => {
|
||||
const taskHandle = addTask()
|
||||
|
||||
try {
|
||||
const agent = await $fetch('/api/agents', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: 'New Agent',
|
||||
systemPrompt: 'You are a helpful assistant.'
|
||||
})
|
||||
});
|
||||
|
||||
if (agent === undefined) throw new Error('Failed to create agent');
|
||||
|
||||
if (agents.value === null) agents.value = [];
|
||||
|
||||
agents.value.push(agent);
|
||||
|
||||
completeTask(taskHandle)
|
||||
|
||||
return agent;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
completeTask(taskHandle)
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let debounceTimeout: NodeJS.Timeout | null = null;
|
||||
const updateAgent = async (id: string, data: Partial<Agent>) => {
|
||||
if (agents.value === null) agents.value = [];
|
||||
|
||||
// update the local state always
|
||||
agents.value = agents.value.map(agent => {
|
||||
if (agent.id === id) return { ...agent, ...data };
|
||||
return agent;
|
||||
});
|
||||
|
||||
// falling edge debounce (when the user stops typing)
|
||||
if (debounceTimeout !== null) clearTimeout(debounceTimeout);
|
||||
debounceTimeout = setTimeout(async () => {
|
||||
const taskHandle = addTask()
|
||||
|
||||
try {
|
||||
const agent = await $fetch(`/api/agents/${id}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (agent === undefined) throw new Error('Failed to update agent');
|
||||
|
||||
const index = agents.value!.findIndex(agent => agent.id === id);
|
||||
if (index === -1) throw new Error('Agent not found');
|
||||
if (agents.value![index] === null) throw new Error('Agent not found');
|
||||
|
||||
agents.value![index] = agent;
|
||||
|
||||
completeTask(taskHandle)
|
||||
|
||||
return agent;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
completeTask(taskHandle)
|
||||
return;
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
return { agents, createAgent, activeAgent, updateAgent };
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
export const useClickOutside = (target: Ref<HTMLElement | null>, callback: () => void) => {
|
||||
const onClick = (event: MouseEvent) => {
|
||||
if (target.value && !target.value.contains(event.target as Node)) {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', onClick)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', onClick)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
export const useKeyboardShortcuts = () => {
|
||||
const { toggle: toggleSidebar } = useSidebar();
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
// Ctrl+[ to collapse sidebar
|
||||
if (event.ctrlKey && event.key === '[') {
|
||||
event.preventDefault();
|
||||
toggleSidebar();
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
});
|
||||
|
||||
return {
|
||||
handleKeyDown
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
export const useSettings = () => {
|
||||
const open = useState<boolean>('settings:open', () => false)
|
||||
const currentPage = useState<string>('settings:currentPage', () => 'page1')
|
||||
|
||||
const toggle = () => { open.value = !open.value }
|
||||
const setPage = (page: string) => { currentPage.value = page }
|
||||
const close = () => {
|
||||
open.value = false
|
||||
currentPage.value = 'page1'
|
||||
}
|
||||
|
||||
return { open, currentPage, toggle, setPage, close }
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
export const useSidebar = () => {
|
||||
const open = useState<boolean>('sidebar:open', () => true)
|
||||
const sidebarWidth = useState<number>('sidebar:width', () => {
|
||||
return Number(useCookie('sidebar:width', { default: () => "226", maxAge: 60 * 60 * 24 * 30 }).value)
|
||||
})
|
||||
|
||||
// I still want the state to update when the cookie change, like it does for the theme cookies
|
||||
// but I dont want to use the cookie value as the state value because then when we change the
|
||||
// cookie value, we thrash the hell out of the cookie and gobble CPU cycles
|
||||
watch(useCookie('sidebar:width'), (value) => {
|
||||
console.log(value)
|
||||
sidebarWidth.value = Number(value)
|
||||
})
|
||||
|
||||
const toggle = () => { open.value = !open.value }
|
||||
const close = () => { open.value = false }
|
||||
const openSidebar = () => { open.value = true }
|
||||
|
||||
const resize = (width: number) => {
|
||||
const minWidth = 200
|
||||
const maxWidth = 400
|
||||
const clampedWidth = Math.max(minWidth, Math.min(maxWidth, width))
|
||||
sidebarWidth.value = clampedWidth
|
||||
}
|
||||
|
||||
const saveWidth = () => {
|
||||
useCookie('sidebar:width').value = sidebarWidth.value.toString()
|
||||
}
|
||||
|
||||
return { open, toggle, close, openSidebar, sidebarWidth: readonly(sidebarWidth), resize, saveWidth }
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
type TaskHandle = number
|
||||
|
||||
export const useTasks = () => {
|
||||
const taskQueue = useState<Set<number>>('spinner:taskQueue', () => new Set())
|
||||
const taskId = useState<number>('spinner:taskId', () => 1)
|
||||
const hasTasks = computed(() => taskQueue.value.size > 0)
|
||||
|
||||
const addTask = (): TaskHandle => {
|
||||
const handle = taskId.value++
|
||||
taskQueue.value = new Set(taskQueue.value).add(handle)
|
||||
return handle
|
||||
}
|
||||
|
||||
const completeTask = (handle: TaskHandle) => {
|
||||
const newSet = new Set(taskQueue.value)
|
||||
newSet.delete(handle)
|
||||
taskQueue.value = newSet
|
||||
}
|
||||
|
||||
return { hasTasks, addTask, completeTask }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export const useTheme = () => {
|
||||
const accent = useCookie('accent', { default: () => 'violet', maxAge: 60 * 60 * 24 * 365 })
|
||||
const neutral = useCookie('neutral', { default: () => 'zinc', maxAge: 60 * 60 * 24 * 365 })
|
||||
// disable hinting by default
|
||||
const hinting = useCookie('hinting', { default: () => '0', maxAge: 60 * 60 * 24 * 365 })
|
||||
|
||||
return {
|
||||
accent,
|
||||
neutral,
|
||||
hinting,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { Topic } from "~~/types";
|
||||
|
||||
export const useTopics = async () => {
|
||||
const fetchingTopics = ref(false);
|
||||
const topics: Ref<Topic[] | null> = useState('topics', () => null);
|
||||
const activeTopic = computed(() => {
|
||||
if (topics.value === null) return;
|
||||
|
||||
const routeId = useRoute().query.topicId;
|
||||
if (routeId === undefined) return;
|
||||
|
||||
const topic = topics.value.find(topic => topic.id === routeId);
|
||||
if (topic === undefined) return;
|
||||
|
||||
return topic;
|
||||
});
|
||||
|
||||
const refreshTopics = async () => {
|
||||
if (fetchingTopics.value) return;
|
||||
fetchingTopics.value = true;
|
||||
|
||||
const { data, error } = await useFetch('/api/topics');
|
||||
if (error.value) throw error;
|
||||
topics.value = data.value!;
|
||||
|
||||
fetchingTopics.value = false;
|
||||
}
|
||||
|
||||
if (topics.value === null) await refreshTopics();
|
||||
|
||||
|
||||
const createTopic = async (name: string, agentId: string) => {
|
||||
const res = await fetch('/api/topics', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ name, agentId })
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to create topic')
|
||||
}
|
||||
|
||||
return res.json()
|
||||
}
|
||||
|
||||
return { createTopic, activeTopic, topics }
|
||||
}
|
||||
Reference in New Issue
Block a user