streaming, markdown, model selecting, and lots more
This commit is contained in:
@@ -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<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,
|
||||
}
|
||||
}
|
||||
+10
-103
@@ -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<Agent[] | null> = 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<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 };
|
||||
}
|
||||
return {
|
||||
agents,
|
||||
getAgent,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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<string | null>('appState:activeAgentId', () => null);
|
||||
const activeTopicId = useState<string | null>('appState:activeTopicId', () => null);
|
||||
|
||||
// User data
|
||||
const user = useState<User | null>('appState:user', () => null);
|
||||
const session = useState<Session | null>('appState:session', () => null);
|
||||
|
||||
// Loading states
|
||||
const isInitializing = useState<boolean>('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
|
||||
};
|
||||
};
|
||||
@@ -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<InferSessionFromClient<BetterAuthClientOptions> | null>('auth:session', () => null);
|
||||
const user = useState<InferUserFromClient<BetterAuthClientOptions> | 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<BetterAuthClientOptions>;
|
||||
user: InferUserFromClient<BetterAuthClientOptions>;
|
||||
} | null = null;
|
||||
if (import.meta.server) {
|
||||
data =
|
||||
(
|
||||
await useFetch<{
|
||||
session: InferSessionFromClient<BetterAuthClientOptions>;
|
||||
user: InferUserFromClient<BetterAuthClientOptions>;
|
||||
}>('/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,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
import { ref, watch, onUnmounted, type Ref } from 'vue';
|
||||
|
||||
export function useAutoScroll(elementRef: Ref<HTMLElement | null>) {
|
||||
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,
|
||||
};
|
||||
}
|
||||
@@ -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<typeof schema, 'messages'> & { parts: (Entity<typeof schema, 'message_parts'> & { toolCall: Entity<typeof schema, 'tool_calls'> | 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<typeof schema, 'agents'>, messages: Readonly<Message[]>) => {
|
||||
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<typeof schema, 'topics'>,
|
||||
topicMessages: Message[],
|
||||
agent: Entity<typeof schema, 'agents'>,
|
||||
provider: Entity<typeof schema, 'providers'>,
|
||||
model: Entity<typeof schema, 'models'>,
|
||||
) => {
|
||||
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,
|
||||
};
|
||||
}
|
||||
@@ -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<HTMLElement | null>, 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)
|
||||
})
|
||||
}
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', onClick);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
onMounted(() => {
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
});
|
||||
|
||||
return {
|
||||
handleKeyDown,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { schema } from '#triplit/schema';
|
||||
import type { Entity } from '@triplit/client';
|
||||
|
||||
export type ModelWithProvider = Entity<typeof schema, 'models'> & {
|
||||
provider: Entity<typeof schema, 'providers'>;
|
||||
};
|
||||
|
||||
export type ProviderWithModels = Entity<typeof schema, 'providers'> & {
|
||||
models: Entity<typeof schema, 'models'>[];
|
||||
};
|
||||
|
||||
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<ProviderWithModels[]>(() => {
|
||||
// if (!providers.value) return [];
|
||||
|
||||
// return (providers.value as unknown as ProviderWithModels[]).filter(
|
||||
// (provider: ProviderWithModels) => provider.models && provider.models.length > 0
|
||||
// );
|
||||
// });
|
||||
|
||||
const allEnabledModels = computed<ModelWithProvider[]>(() => {
|
||||
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,
|
||||
};
|
||||
};
|
||||
@@ -1,13 +1,28 @@
|
||||
export const useSettings = () => {
|
||||
const open = useState<boolean>('settings:open', () => false)
|
||||
const currentPage = useState<string>('settings:currentPage', () => 'page1')
|
||||
const open = useState<boolean>('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 }
|
||||
}
|
||||
return { open, currentPage, pageParams, toggle, setPage, close };
|
||||
};
|
||||
|
||||
@@ -1,31 +1,49 @@
|
||||
export const useSidebar = () => {
|
||||
const open = useState<boolean>('sidebar:open', () => true)
|
||||
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)
|
||||
})
|
||||
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 }
|
||||
}
|
||||
return {
|
||||
open,
|
||||
toggle,
|
||||
close,
|
||||
openSidebar,
|
||||
sidebarWidth: readonly(sidebarWidth),
|
||||
resize,
|
||||
saveWidth,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
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 }
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import type { Topic } from "~~/types";
|
||||
|
||||
export const useTopics = async () => {
|
||||
const appState = useAppState()
|
||||
const fetchingTopics = ref(false);
|
||||
const topics: Ref<any[] | null> = 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 }
|
||||
}
|
||||
Reference in New Issue
Block a user