@@ -95,7 +107,7 @@ const navKind = computed(() => {
@@ -104,15 +116,24 @@ const navKind = computed(() => {
-
-
-
diff --git a/app/components/Slider.vue b/app/components/Slider.vue
new file mode 100644
index 0000000..9a568d2
--- /dev/null
+++ b/app/components/Slider.vue
@@ -0,0 +1,77 @@
+
+
+
+ $emit('click', e)"
+ :aria-checked="active" :data-state="(active) ? 'checked' : 'unchecked'">
+
+
+
+
+
\ No newline at end of file
diff --git a/app/components/ThemeSwitcher.vue b/app/components/ThemeSwitcher.vue
index dae9844..f93aea4 100644
--- a/app/components/ThemeSwitcher.vue
+++ b/app/components/ThemeSwitcher.vue
@@ -1,36 +1,35 @@
-
-
+
+
diff --git a/app/composables/auth.ts b/app/composables/auth.ts
deleted file mode 100644
index 2d0d8fa..0000000
--- a/app/composables/auth.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-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 | null>('auth:session', () => null)
- const user = useState | 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,
- }
-}
\ No newline at end of file
diff --git a/app/composables/useAgents.ts b/app/composables/useAgents.ts
index 19cbede..5f0ee18 100644
--- a/app/composables/useAgents.ts
+++ b/app/composables/useAgents.ts
@@ -1,107 +1,14 @@
-import { ref } from 'vue'
-import type { Agent } from '~~/types'
-
export const useAgents = async () => {
- const { addTask, completeTask } = useTasks()
- const appState = useAppState()
+ const triplit = useTriplitClient();
- const fetchingAgents = ref(false);
- const agents: Ref = useState('agents', () => null);
-
- const activeAgent = computed(() => {
- if (agents.value === null) return;
- if (!appState.activeAgentId.value) return;
+ const { results: agents } = await useQuery('agents', triplit, triplit.query('agents').Include('topics'));
- const agent = agents.value.find(agent => agent.id === appState.activeAgentId.value);
- return agent;
- });
+ const getAgent = (id: string) => {
+ return agents.value?.find((a) => a.id === id);
+ };
- 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) => {
- 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 };
-}
\ No newline at end of file
+ return {
+ agents,
+ getAgent,
+ };
+};
diff --git a/app/composables/useAppState.ts b/app/composables/useAppState.ts
deleted file mode 100644
index 37c5249..0000000
--- a/app/composables/useAppState.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-import type { User, Session } from 'better-auth/types';
-
-/**
- * Central application state composable
- * Manages navigation context and critical app-level state
- * This is the single source of truth for "what am I viewing"
- */
-export const useAppState = () => {
- // Current navigation context
- const activeAgentId = useState('appState:activeAgentId', () => null);
- const activeTopicId = useState('appState:activeTopicId', () => null);
-
- // User data
- const user = useState('appState:user', () => null);
- const session = useState('appState:session', () => null);
-
- // Loading states
- const isInitializing = useState('appState:isInitializing', () => true);
- const generationInProgress = useState<{ generationId: string } | null>('appState:generationInProgress', () => null);
-
- /**
- * Set the active agent and clear the topic
- */
- const setActiveAgent = (agentId: string | null | undefined) => {
- activeAgentId.value = agentId || null;
- // Clear topic when switching agents
- activeTopicId.value = null;
- };
-
- /**
- * Set the active topic
- */
- const setActiveTopic = (topicId: string | null | undefined) => {
- activeTopicId.value = topicId || null;
- };
-
- /**
- * Set user session data
- */
- const setUser = (userData: User | null) => {
- user.value = userData;
- };
-
- /**
- * Set session
- */
- const setSession = (sessionData: Session | null) => {
- session.value = sessionData;
- };
-
- /**
- * Mark initialization complete
- */
- const markInitialized = () => {
- isInitializing.value = false;
- };
-
- /**
- * Start a generation
- */
- const startGeneration = (generationId: string) => {
- generationInProgress.value = { generationId };
- };
-
- /**
- * End current generation
- */
- const endGeneration = () => {
- generationInProgress.value = null;
- };
-
- return {
- // State
- activeAgentId,
- activeTopicId,
- user,
- session,
- isInitializing,
- generationInProgress,
-
- // Actions
- setActiveAgent,
- setActiveTopic,
- setUser,
- setSession,
- markInitialized,
- startGeneration,
- endGeneration
- };
-};
diff --git a/app/composables/useAuth.ts b/app/composables/useAuth.ts
new file mode 100644
index 0000000..64f39e9
--- /dev/null
+++ b/app/composables/useAuth.ts
@@ -0,0 +1,66 @@
+import type { BetterAuthClientOptions, InferSessionFromClient, InferUserFromClient } from 'better-auth/client';
+import { authClient } from '~~/lib/auth-client';
+
+export const useAuth = () => {
+ const session = useState | null>('auth:session', () => null);
+ const user = useState | null>('auth:user', () => null);
+ const sessionFetching = import.meta.server ? ref(false) : useState('auth:sessionFetching', () => false);
+
+ const fetchSession = async () => {
+ if (sessionFetching.value) {
+ console.log('already fetching session');
+ return;
+ }
+ sessionFetching.value = true;
+ let data: {
+ session: InferSessionFromClient;
+ user: InferUserFromClient;
+ } | null = null;
+ if (import.meta.server) {
+ data =
+ (
+ await useFetch<{
+ session: InferSessionFromClient;
+ user: InferUserFromClient;
+ }>('/api/auth/get-session')
+ ).data.value ?? null;
+ } else {
+ data = (await authClient.getSession()).data;
+ }
+ session.value = data?.session || null;
+ user.value = data?.user || null;
+ sessionFetching.value = false;
+ return data;
+ };
+
+ if (import.meta.client) {
+ authClient.$store.listen('$sessionSignal', async (signal) => {
+ if (!signal) return;
+ await fetchSession();
+
+ if (!session.value) return;
+
+ const triplit = useTriplitClient();
+ if ('updateOptions' in triplit) {
+ triplit.updateOptions({
+ token: session.value.token,
+ });
+ }
+ });
+ }
+
+ return {
+ session,
+ user,
+ loggedIn: computed(() => !!session.value),
+ signIn: authClient.signIn,
+ signUp: authClient.signUp,
+ async signOut() {
+ await authClient.signOut();
+ session.value = null;
+ user.value = null;
+ return navigateTo('/auth/login');
+ },
+ fetchSession,
+ };
+};
diff --git a/app/composables/useAutoScroll.ts b/app/composables/useAutoScroll.ts
new file mode 100644
index 0000000..7ab1918
--- /dev/null
+++ b/app/composables/useAutoScroll.ts
@@ -0,0 +1,64 @@
+import { ref, watch, onUnmounted, type Ref } from 'vue';
+
+export function useAutoScroll(elementRef: Ref) {
+ const userIsScrollingUp = ref(false);
+ const THRESHOLD = 50;
+
+ const isAtBottom = () => {
+ const el = elementRef.value;
+ if (!el) return false;
+ const distanceToBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
+ return distanceToBottom <= THRESHOLD;
+ };
+
+ const scrollToBottom = (behavior: ScrollBehavior = 'auto') => {
+ const el = elementRef.value;
+ if (!el) return;
+ el.scrollTo({
+ top: el.scrollHeight,
+ behavior,
+ });
+ };
+
+ const handleScroll = () => {
+ const el = elementRef.value;
+ if (!el) return;
+
+ userIsScrollingUp.value = !isAtBottom();
+ };
+
+ let observer: MutationObserver | null = null;
+
+ watch(elementRef, (newEl, oldEl) => {
+ if (oldEl) {
+ oldEl.removeEventListener('scroll', handleScroll);
+ observer?.disconnect();
+ }
+
+ if (newEl) {
+ newEl.addEventListener('scroll', handleScroll, { passive: true });
+
+ observer = new MutationObserver(() => {
+ if (!userIsScrollingUp.value) {
+ scrollToBottom();
+ }
+ });
+
+ observer.observe(newEl, {
+ childList: true,
+ subtree: true,
+ characterData: true
+ });
+ }
+ });
+
+ onUnmounted(() => {
+ elementRef.value?.removeEventListener('scroll', handleScroll);
+ observer?.disconnect();
+ });
+
+ return {
+ userIsScrollingUp,
+ scrollToBottom,
+ };
+}
\ No newline at end of file
diff --git a/app/composables/useChat.ts b/app/composables/useChat.ts
new file mode 100644
index 0000000..eef4a19
--- /dev/null
+++ b/app/composables/useChat.ts
@@ -0,0 +1,232 @@
+import type schema from "#triplit/schema";
+import type { Entity } from "@triplit/client";
+import type { ModelMessage } from "ai";
+import { decrypt, base64ToUint8Array } from "~/utils/crypto";
+
+export type Message = Entity & { parts: (Entity & { toolCall: Entity | null } | undefined)[] };
+
+export const useChat = (agentId: string) => {
+ const triplit = useTriplitClient();
+
+ const createTopic = async () => {
+ const { user } = useAuth();
+ if (!user.value) {
+ console.error('No user');
+ return;
+ }
+
+ const newTopic = await triplit.insert('topics', {
+ name: 'New Topic',
+ userId: user.value.id,
+ agentId,
+ createdAt: new Date().toISOString(),
+ });
+
+ if ('flush' in triplit) {
+ await triplit.flush();
+ }
+
+ return newTopic;
+ };
+
+ const marshallMessages = (agent: Entity, messages: Readonly) => {
+ const marshalledMessages: ModelMessage[] = [];
+
+ if (agent && agent.systemPrompt) {
+ marshalledMessages.push({
+ role: 'system',
+ content: agent.systemPrompt,
+ });
+ }
+
+ messages.forEach((message) => {
+ switch (message.role) {
+ case 'user':
+ marshalledMessages.push({
+ role: 'user',
+ // TODO: when we have images or files, this is where we need to handle them
+ content: message.content,
+ });
+ break;
+ case 'assistant':
+ message.parts.forEach((part) => {
+ if (!part) throw new Error('Part is undefined');
+
+ switch (part.type) {
+ case 'text':
+ case 'reasoning': {
+ marshalledMessages.push({
+ role: 'assistant',
+ content: part.content,
+ });
+ break;
+ }
+ case 'tool-call': {
+ if (part.toolCall === null) throw new Error('Tool call is null');
+
+ if (part.toolCall.status === 'pending') {
+ throw new Error(
+ 'Marshalling tool call that is still pending. This is likely a UI bug if this happens.',
+ );
+ }
+
+ let inputValue: string = '';
+
+ switch (typeof part.toolCall.input!.value) {
+ case 'string':
+ inputValue = part.toolCall.input!.value;
+ break;
+ case 'object':
+ inputValue = JSON.stringify(part.toolCall.input!.value, null, 2);
+ break;
+ }
+
+ marshalledMessages.push({
+ role: 'assistant',
+ content: [
+ {
+ type: 'tool-call',
+ toolCallId: part.toolCall.id,
+ toolName: part.toolCall.toolName,
+ input: inputValue,
+ },
+ ],
+ providerOptions: part.providerOptions,
+ });
+
+ if (part.toolCall.status === 'failed') {
+ let failureType: 'error-text' | 'error-json';
+ let failureValue: string;
+
+ if (part.toolCall.error === null || part.toolCall.error === undefined) {
+ failureType = 'error-text';
+ failureValue = 'An unknown error occurred';
+ } else {
+ switch (part.toolCall.error!.type) {
+ case 'text':
+ failureType = 'error-text';
+ failureValue = part.toolCall.error!.value;
+ break;
+ case 'json':
+ failureType = 'error-json';
+ failureValue = JSON.stringify(part.toolCall.error!.value, null, 2);
+ break;
+ }
+ }
+
+ marshalledMessages.push({
+ role: 'tool',
+ content: [
+ {
+ type: 'tool-result',
+ toolCallId: part.toolCall.id,
+ toolName: part.toolCall.toolName,
+ output: {
+ type: failureType,
+ value: failureValue,
+ },
+ },
+ ],
+ providerOptions: part.providerOptions,
+ });
+ break;
+ }
+
+ if (part.toolCall.status === 'completed') {
+ marshalledMessages.push({
+ role: 'tool',
+ content: [
+ {
+ type: 'tool-result',
+ toolCallId: part.toolCall.id,
+ toolName: part.toolCall.toolName,
+ output: {
+ type: 'json',
+ value: JSON.stringify(part.toolCall.output!.value, null, 2),
+ },
+ },
+ ],
+ providerOptions: part.providerOptions,
+ });
+ break;
+ }
+ } break;
+ default:
+ throw new Error(`Unknown part type: ${part.type}`);
+ }
+ });
+ break;
+ default:
+ throw new Error(`Unknown message role: ${message.role}`);
+ }
+ });
+
+ return marshalledMessages;
+ };
+
+ const sendMessage = async (
+ message: string,
+ topic: Entity,
+ topicMessages: Message[],
+ agent: Entity,
+ provider: Entity,
+ model: Entity,
+ ) => {
+ const newMessage = await triplit.insert('messages', {
+ topicId: topic.id,
+ createdAt: new Date().toISOString(),
+ content: message,
+ role: 'user',
+ }) as Message;
+
+ const messages = marshallMessages(
+ agent,
+ topicMessages.concat(newMessage)
+ );
+
+
+ let providerApiKey: string | undefined = undefined;
+ if (provider.config.apiKey !== undefined) {
+ const key = await crypto.subtle.importKey(
+ "jwk",
+ JSON.parse(window.localStorage.getItem("encryptionKey")!),
+ "AES-GCM",
+ false,
+ ["encrypt", "decrypt"]
+ )
+
+ providerApiKey = await decrypt(
+ key,
+ base64ToUint8Array(provider.config.apiKey)
+ );
+ }
+
+ return $fetch('/api/chat/generate', {
+ method: 'POST',
+ body: {
+ messages,
+ topicId: topic.id,
+ model: {
+ providerId: provider.id,
+ modelId: model.id,
+ args: {
+ temperature: 0.7,
+ max_tokens: 100,
+ top_p: 1,
+ frequency_penalty: 0,
+ presence_penalty: 0,
+ },
+ },
+ providerApiKey: providerApiKey,
+ },
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+ };
+
+ return {
+ sendMessage,
+ createTopic,
+ };
+}
\ No newline at end of file
diff --git a/app/composables/useClickOutside.ts b/app/composables/useClickOutside.ts
index b5f5416..8f6fd51 100644
--- a/app/composables/useClickOutside.ts
+++ b/app/composables/useClickOutside.ts
@@ -1,18 +1,18 @@
-import { onMounted, onUnmounted } from 'vue'
-import type { Ref } from 'vue'
+import type { Ref } from 'vue';
+import { onMounted, onUnmounted } from 'vue';
export const useClickOutside = (target: Ref, callback: () => void) => {
- const onClick = (event: MouseEvent) => {
- if (target.value && !target.value.contains(event.target as Node)) {
- callback()
- }
- }
+ const onClick = (event: MouseEvent) => {
+ if (target.value && !target.value.contains(event.target as Node)) {
+ callback();
+ }
+ };
- onMounted(() => {
- document.addEventListener('click', onClick)
- })
+ onMounted(() => {
+ document.addEventListener('click', onClick);
+ });
- onUnmounted(() => {
- document.removeEventListener('click', onClick)
- })
-}
\ No newline at end of file
+ onUnmounted(() => {
+ document.removeEventListener('click', onClick);
+ });
+};
diff --git a/app/composables/useFillIds.ts b/app/composables/useFillIds.ts
new file mode 100644
index 0000000..5291c70
--- /dev/null
+++ b/app/composables/useFillIds.ts
@@ -0,0 +1,12 @@
+export const useFillIds = (namespace: string, length: number) => {
+ const instanceId = useId();
+
+ return Array.from({ length }, (_, i) => {
+ const id = `veridian-icons-${namespace}-${instanceId}-${i}`;
+
+ return {
+ fill: `url(#${id})`,
+ id,
+ };
+ });
+}
diff --git a/app/composables/useKeyboardShortcuts.ts b/app/composables/useKeyboardShortcuts.ts
index f760b20..9bff6f2 100644
--- a/app/composables/useKeyboardShortcuts.ts
+++ b/app/composables/useKeyboardShortcuts.ts
@@ -1,23 +1,35 @@
export const useKeyboardShortcuts = () => {
- const { toggle: toggleSidebar } = useSidebar();
+ const { toggle: toggleSidebar } = useSidebar();
+ const { toggle: openSettings, open: isSettingsOpen } = useSettings();
- const handleKeyDown = (event: KeyboardEvent) => {
- // Ctrl+[ to collapse sidebar
- if (event.ctrlKey && event.key === '[') {
- event.preventDefault();
- toggleSidebar();
- }
- };
+ const handleKeyDown = (event: KeyboardEvent) => {
+ // Ctrl+[ to collapse sidebar
+ if (event.ctrlKey && event.key === '[') {
+ event.preventDefault();
+ toggleSidebar();
+ return;
+ }
- onMounted(() => {
- document.addEventListener('keydown', handleKeyDown);
- });
+ if (event.ctrlKey && event.key === ',') {
+ event.preventDefault();
+ if (isSettingsOpen.value) {
+ return;
+ }
- onUnmounted(() => {
- document.removeEventListener('keydown', handleKeyDown);
- });
+ openSettings();
+ return;
+ }
+ };
- return {
- handleKeyDown
- };
-};
\ No newline at end of file
+ onMounted(() => {
+ document.addEventListener('keydown', handleKeyDown);
+ });
+
+ onUnmounted(() => {
+ document.removeEventListener('keydown', handleKeyDown);
+ });
+
+ return {
+ handleKeyDown,
+ };
+};
diff --git a/app/composables/useModels.ts b/app/composables/useModels.ts
new file mode 100644
index 0000000..43372be
--- /dev/null
+++ b/app/composables/useModels.ts
@@ -0,0 +1,51 @@
+import { schema } from '#triplit/schema';
+import type { Entity } from '@triplit/client';
+
+export type ModelWithProvider = Entity & {
+ provider: Entity;
+};
+
+export type ProviderWithModels = Entity & {
+ models: Entity[];
+};
+
+export const useModels = async () => {
+ const triplit = useTriplitClient();
+
+ const providersQuery = triplit
+ .query('providers')
+ .Include('models')
+
+ const { results: providers, unsubscribe } = await useQuery('providers', triplit, providersQuery);
+
+ console.log("GET PROVIDERS", providers.value);
+
+ // const enabledProvidersWithModels = computed(() => {
+ // if (!providers.value) return [];
+
+ // return (providers.value as unknown as ProviderWithModels[]).filter(
+ // (provider: ProviderWithModels) => provider.models && provider.models.length > 0
+ // );
+ // });
+
+ const allEnabledModels = computed(() => {
+ return providers.value?.flatMap((provider) =>
+ provider.models.map((model) => ({
+ ...model,
+ provider,
+ }))
+ );
+ });
+
+ const getFirstAvailableModel = (): ModelWithProvider | null => {
+ if (allEnabledModels.value.length === 0) return null;
+ return allEnabledModels.value[0]!;
+ };
+
+ return {
+ providers,
+ allModels: allEnabledModels,
+ getFirstAvailableModel,
+ unsubscribe,
+ };
+};
diff --git a/app/composables/useSettings.ts b/app/composables/useSettings.ts
index 9166779..1ff1e0d 100644
--- a/app/composables/useSettings.ts
+++ b/app/composables/useSettings.ts
@@ -1,13 +1,28 @@
export const useSettings = () => {
- const open = useState('settings:open', () => false)
- const currentPage = useState('settings:currentPage', () => 'page1')
+ const open = useState('settings:open', () => false);
+ const currentPage = useState('settings-page', () => 'general');
+ const pageParams = useState('settings-params', () => [] as string[]);
+
+ const toggle = () => {
+ open.value = !open.value;
+ };
+
+ const setPage = (id: string, params?: string | string[]) => {
+ currentPage.value = id;
+ if (params) {
+ if (typeof params === 'string') {
+ params = [params];
+ }
+ pageParams.value = params;
+ } else {
+ pageParams.value = [];
+ }
+ };
- const toggle = () => { open.value = !open.value }
- const setPage = (page: string) => { currentPage.value = page }
const close = () => {
- open.value = false
- currentPage.value = 'page1'
- }
+ open.value = false;
+ currentPage.value = 'page1';
+ };
- return { open, currentPage, toggle, setPage, close }
-}
\ No newline at end of file
+ return { open, currentPage, pageParams, toggle, setPage, close };
+};
diff --git a/app/composables/useSidebar.ts b/app/composables/useSidebar.ts
index f4e0966..eb5d8ef 100644
--- a/app/composables/useSidebar.ts
+++ b/app/composables/useSidebar.ts
@@ -1,31 +1,49 @@
export const useSidebar = () => {
- const open = useState('sidebar:open', () => true)
+ const open = useState('sidebar:open', () => true);
const sidebarWidth = useState('sidebar:width', () => {
- return Number(useCookie('sidebar:width', { default: () => "226", maxAge: 60 * 60 * 24 * 30 }).value)
- })
+ 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)
- })
+ sidebarWidth.value = Number(value);
+ });
- const toggle = () => { open.value = !open.value }
- const close = () => { open.value = false }
- const openSidebar = () => { open.value = true }
+ 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 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()
- }
+ useCookie('sidebar:width').value = sidebarWidth.value.toString();
+ };
- return { open, toggle, close, openSidebar, sidebarWidth: readonly(sidebarWidth), resize, saveWidth }
-}
\ No newline at end of file
+ return {
+ open,
+ toggle,
+ close,
+ openSidebar,
+ sidebarWidth: readonly(sidebarWidth),
+ resize,
+ saveWidth,
+ };
+};
diff --git a/app/composables/useTasks.ts b/app/composables/useTasks.ts
deleted file mode 100644
index 7a2a34e..0000000
--- a/app/composables/useTasks.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-type TaskHandle = number
-
-export const useTasks = () => {
- const taskQueue = useState>('spinner:taskQueue', () => new Set())
- const taskId = useState('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 }
-}
\ No newline at end of file
diff --git a/app/composables/useTheme.ts b/app/composables/useTheme.ts
index 2824b1c..d2ad4e3 100644
--- a/app/composables/useTheme.ts
+++ b/app/composables/useTheme.ts
@@ -1,12 +1,21 @@
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 })
+ 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 })
+ const hinting = useCookie('hinting', {
+ default: () => '0',
+ maxAge: 60 * 60 * 24 * 365,
+ });
return {
accent,
neutral,
hinting,
- }
-}
+ };
+};
diff --git a/app/composables/useTopics.ts b/app/composables/useTopics.ts
deleted file mode 100644
index b2e2663..0000000
--- a/app/composables/useTopics.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import type { Topic } from "~~/types";
-
-export const useTopics = async () => {
- const appState = useAppState()
- const fetchingTopics = ref(false);
- const topics: Ref = useState('topics', () => null);
-
- /**
- * Compute topics for the currently active agent
- */
- const topicsForActiveAgent = computed(() => {
- if (topics.value === null || !appState.activeAgentId.value) return [];
- return topics.value.filter(topic => topic.agentId === appState.activeAgentId.value);
- });
-
- const activeTopic = computed(() => {
- if (topicsForActiveAgent.value.length === 0) return;
- if (!appState.activeTopicId.value) return;
-
- const topic = topicsForActiveAgent.value.find(topic => topic.id === appState.activeTopicId.value);
- return topic;
- });
-
- const refreshTopics = async () => {
- if (fetchingTopics.value) return;
- fetchingTopics.value = true;
-
- try {
- const { data, error } = await useFetch('/api/topics');
- if (error.value) throw error;
- topics.value = data.value!;
- } finally {
- 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')
- }
-
- const newTopic = await res.json()
- if (topics.value === null) topics.value = []
- topics.value.push(newTopic)
- return newTopic
- }
-
- return { createTopic, activeTopic, topics, topicsForActiveAgent, fetchingTopics, refreshTopics }
-}
\ No newline at end of file
diff --git a/app/layouts/default.vue b/app/layouts/default.vue
index 72a7254..7bee9ec 100644
--- a/app/layouts/default.vue
+++ b/app/layouts/default.vue
@@ -1,6 +1,8 @@
@@ -10,19 +12,18 @@ useKeyboardShortcuts()
+ class="flex p-2 rounded-lg text-[var(--color-text)] bg-transparent hover:bg-[var(--color-highlight)] transition-colors">
-
+
-
diff --git a/app/middleware/auth.global.ts b/app/middleware/auth.global.ts
index 332333e..27663ca 100644
--- a/app/middleware/auth.global.ts
+++ b/app/middleware/auth.global.ts
@@ -1,13 +1,19 @@
export default defineNuxtRouteMiddleware(async (to) => {
- const { loggedIn } = useAuth()
+ const { session, fetchSession } = useAuth();
- // if authenticated, and on a signin/signup page, redirect to home page
- if (to.path.toLowerCase().includes('/auth/') && loggedIn.value) {
- return navigateTo('/')
- }
+ if (!session.value) {
+ await fetchSession();
+ }
- // If not authenticated, and not on a signin/signup page, redirect to login page
- if (!loggedIn.value && !to.path.toLowerCase().includes('/auth/')) {
- return navigateTo('/auth/login')
- }
-})
\ No newline at end of file
+ const loggedIn = computed(() => !!session.value);
+
+ // if authenticated, and on a signin/signup page, redirect to home page
+ if (to.path.toLowerCase().includes("/auth/") && loggedIn.value) {
+ return await navigateTo((to.query.to as string) ?? "/");
+ }
+
+ // If not authenticated, and not on a signin/signup page, redirect to login page
+ if (!loggedIn.value && !to.path.toLowerCase().includes("/auth/")) {
+ return await navigateTo(`/auth/login?to=${to.path}`);
+ }
+});
diff --git a/app/pages/agent/[id]/index.vue b/app/pages/agent/[id]/index.vue
index fba0793..8ec651f 100644
--- a/app/pages/agent/[id]/index.vue
+++ b/app/pages/agent/[id]/index.vue
@@ -1,37 +1,58 @@
+
-
-
-
-
-
{{ activeAgent.name }}
-
Select a topic to continue or create a new one
+
+
+
+
+
+
{{ agent.name }}
+
Select a topic to continue or create a new one
+
-
+
-
\ No newline at end of file
+
diff --git a/app/pages/agent/[id]/profile.vue b/app/pages/agent/[id]/profile.vue
index 7602596..7390868 100644
--- a/app/pages/agent/[id]/profile.vue
+++ b/app/pages/agent/[id]/profile.vue
@@ -1,21 +1,44 @@
-
+
@@ -25,9 +48,10 @@ const handleInput = (e: Event) => {
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" />
-
-
Agent ID:
-
{{ route.params.id }}
+
+
\ No newline at end of file
diff --git a/app/pages/agent/[id]/topic/[topicId].vue b/app/pages/agent/[id]/topic/[topicId].vue
index 45996ae..8e31473 100644
--- a/app/pages/agent/[id]/topic/[topicId].vue
+++ b/app/pages/agent/[id]/topic/[topicId].vue
@@ -1,235 +1,112 @@
-
-
-
-
-
-
-
-
-
-
-
Agent
-
{{ generatingMessage }}
-
+
+
+
+
+
+
-
-
- No messages yet. Start the conversation!
-
-
+
-
+
\ No newline at end of file
diff --git a/app/pages/auth/login.vue b/app/pages/auth/login.vue
index 3cfdaba..8d79107 100644
--- a/app/pages/auth/login.vue
+++ b/app/pages/auth/login.vue
@@ -1,47 +1,46 @@
@@ -103,9 +114,10 @@ const submit = async () => {
-
+
Login
- Dont have an account? Register
+ Dont have an account? Register
\ No newline at end of file
diff --git a/app/pages/auth/register.vue b/app/pages/auth/register.vue
index 52477e5..150c933 100644
--- a/app/pages/auth/register.vue
+++ b/app/pages/auth/register.vue
@@ -1,47 +1,51 @@
@@ -151,5 +166,5 @@ const submit = async () => {
Register
- Already have an account? Login
+ Already have an account? Login
\ No newline at end of file
diff --git a/app/pages/index.vue b/app/pages/index.vue
index 87c1497..d059379 100644
--- a/app/pages/index.vue
+++ b/app/pages/index.vue
@@ -1,44 +1,40 @@