feat: add better provider support, icons, regen, and a lot more
This commit is contained in:
@@ -1,14 +1,34 @@
|
||||
import type schema from "#triplit/schema";
|
||||
import type { Entity } from "@triplit/client";
|
||||
|
||||
export const useAgents = async () => {
|
||||
const triplit = useTriplitClient();
|
||||
|
||||
const { results: agents } = await useQuery('agents', triplit, triplit.query('agents').Include('topics'));
|
||||
const { results: agents, unsubscribe } = await useQuery('agents', triplit, triplit.query('agents').Include('topics'));
|
||||
|
||||
const getAgent = (id: string) => {
|
||||
return agents.value?.find((a) => a.id === id);
|
||||
const createAgent = async (): Promise<Readonly<Entity<typeof schema, 'agents'>> | null> => {
|
||||
const { user } = useAuth();
|
||||
if (!user.value) {
|
||||
console.error('No user');
|
||||
return null;
|
||||
}
|
||||
|
||||
return triplit.insert('agents', {
|
||||
name: 'New Agent',
|
||||
userId: user.value.id,
|
||||
systemPrompt: 'You are a helpful assistant.',
|
||||
defaultModelId: null,
|
||||
imageUrl: null,
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
agents,
|
||||
getAgent,
|
||||
unsubscribe,
|
||||
getAgent: (id: string) => {
|
||||
return agents.value?.find((a: any) => a.id === id);
|
||||
},
|
||||
createAgent,
|
||||
};
|
||||
};
|
||||
|
||||
+169
-51
@@ -1,66 +1,184 @@
|
||||
import type { BetterAuthClientOptions, InferSessionFromClient, InferUserFromClient } from 'better-auth/client';
|
||||
import { authClient } from '~~/lib/auth-client';
|
||||
import type { User, Session } from 'better-auth';
|
||||
import type { Result } from '~~/types/result';
|
||||
import { Ok, Err } from '~~/types/result';
|
||||
import { createAuthClient } from 'better-auth/vue';
|
||||
|
||||
export enum AuthError {
|
||||
NotAuthenticated = 'NOT_AUTHENTICATED',
|
||||
SignInFailed = 'SIGN_IN_FAILED',
|
||||
SignUpFailed = 'SIGN_UP_FAILED',
|
||||
SignOutFailed = 'SIGN_OUT_FAILED',
|
||||
NetworkError = 'NETWORK_ERROR',
|
||||
}
|
||||
|
||||
export interface AuthState {
|
||||
user: User | null;
|
||||
session: Session | null;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
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 url = useRequestURL();
|
||||
const headers = useRequestHeaders();
|
||||
const client = createAuthClient({
|
||||
baseURL: url.origin,
|
||||
fetchOptions: {
|
||||
headers
|
||||
}
|
||||
});
|
||||
|
||||
const fetchSession = async () => {
|
||||
if (sessionFetching.value) {
|
||||
console.log('already fetching session');
|
||||
return;
|
||||
const state = useState<AuthState>('auth:state', () => ({
|
||||
user: null,
|
||||
session: null,
|
||||
isLoading: false,
|
||||
}));
|
||||
|
||||
const isAuthenticated = computed(() => !!state.value.session);
|
||||
const userId = computed(() => state.value.user?.id ?? null);
|
||||
|
||||
const fetchSession = async (): Promise<Result<{ session: Session | null; user: User | null }, AuthError>> => {
|
||||
state.value.isLoading = true;
|
||||
|
||||
try {
|
||||
const { data } = await client.getSession();
|
||||
|
||||
if (data) {
|
||||
state.value.session = data.session;
|
||||
state.value.user = data.user;
|
||||
return Ok({ session: data.session, user: data.user });
|
||||
}
|
||||
|
||||
state.value.session = null;
|
||||
state.value.user = null;
|
||||
return Ok({ session: null, user: null });
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch session:', error);
|
||||
return Err(AuthError.NetworkError);
|
||||
} finally {
|
||||
state.value.isLoading = false;
|
||||
}
|
||||
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();
|
||||
const signIn = async (
|
||||
email: string,
|
||||
password: string
|
||||
): Promise<Result<{ user: User; token: string }, { error: AuthError, data?: any }>> => {
|
||||
state.value.isLoading = true;
|
||||
|
||||
if (!session.value) return;
|
||||
try {
|
||||
const { data, error } = await client.signIn.email({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('Sign in failed:', error);
|
||||
return Err({ error: AuthError.SignInFailed, data: error });
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return Err({ error: AuthError.SignInFailed });
|
||||
}
|
||||
|
||||
state.value.user = data.user;
|
||||
|
||||
const triplit = useTriplitClient();
|
||||
if ('updateOptions' in triplit) {
|
||||
triplit.updateOptions({
|
||||
token: session.value.token,
|
||||
});
|
||||
if ('startSession' in triplit && data.token) {
|
||||
await triplit.startSession(data.token);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
clearNuxtData();
|
||||
|
||||
return Ok({ user: data.user, token: data.token });
|
||||
} catch (err) {
|
||||
console.error('Sign in error:', err);
|
||||
return Err({ error: AuthError.NetworkError });
|
||||
} finally {
|
||||
state.value.isLoading = false;
|
||||
}
|
||||
};
|
||||
|
||||
const signUp = async (
|
||||
email: string,
|
||||
password: string,
|
||||
name: string
|
||||
): Promise<Result<{ user: User; token: string }, { error: AuthError, data?: any }>> => {
|
||||
state.value.isLoading = true;
|
||||
|
||||
try {
|
||||
const { data, error } = await client.signUp.email({
|
||||
email,
|
||||
password,
|
||||
name,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('Sign up failed:', error);
|
||||
return Err({ error: AuthError.SignUpFailed, data: error });
|
||||
}
|
||||
|
||||
if (!data || !data.token) {
|
||||
return Err({ error: AuthError.SignUpFailed });
|
||||
}
|
||||
|
||||
state.value.user = data.user;
|
||||
|
||||
const triplit = useTriplitClient();
|
||||
if ('startSession' in triplit) {
|
||||
await triplit.startSession(data.token);
|
||||
}
|
||||
|
||||
clearNuxtData();
|
||||
|
||||
return Ok({ user: data.user, token: data.token });
|
||||
} catch (err) {
|
||||
console.error('Sign up error:', err);
|
||||
return Err({ error: AuthError.NetworkError });
|
||||
} finally {
|
||||
state.value.isLoading = false;
|
||||
}
|
||||
};
|
||||
|
||||
const signOut = async (): Promise<Result<void, AuthError>> => {
|
||||
state.value.isLoading = true;
|
||||
|
||||
try {
|
||||
const { error } = await client.signOut();
|
||||
|
||||
if (error) {
|
||||
console.error('Sign out failed:', error);
|
||||
return Err(AuthError.SignOutFailed);
|
||||
}
|
||||
|
||||
state.value.user = null;
|
||||
state.value.session = null;
|
||||
|
||||
const triplit = useTriplitClient();
|
||||
if ('disconnect' in triplit) {
|
||||
triplit.disconnect();
|
||||
}
|
||||
|
||||
clearNuxtData();
|
||||
|
||||
return Ok(undefined);
|
||||
} catch (err) {
|
||||
console.error('Sign out error:', err);
|
||||
return Err(AuthError.NetworkError);
|
||||
} finally {
|
||||
state.value.isLoading = false;
|
||||
}
|
||||
};
|
||||
|
||||
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');
|
||||
},
|
||||
client,
|
||||
user: computed(() => state.value.user),
|
||||
session: computed(() => state.value.session),
|
||||
isLoading: computed(() => state.value.isLoading),
|
||||
isAuthenticated,
|
||||
userId,
|
||||
fetchSession,
|
||||
signIn,
|
||||
signUp,
|
||||
signOut,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,53 +1,68 @@
|
||||
import { ref, watch, onUnmounted, type Ref } from 'vue';
|
||||
|
||||
export function useAutoScroll(elementRef: Ref<HTMLElement | null>) {
|
||||
const userIsScrollingUp = ref(false);
|
||||
const THRESHOLD = 50;
|
||||
export function useAutoScroll(elementRef: Ref<HTMLElement | null>, options: {
|
||||
threshold?: number;
|
||||
} = {}) {
|
||||
const { threshold = 80 } = options;
|
||||
|
||||
const isAtBottom = () => {
|
||||
const el = elementRef.value;
|
||||
if (!el) return false;
|
||||
const distanceToBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
|
||||
return distanceToBottom <= THRESHOLD;
|
||||
};
|
||||
const isUserScrollingUp = ref(false);
|
||||
const shouldAutoScroll = ref(true);
|
||||
|
||||
const scrollToBottom = (behavior: ScrollBehavior = 'auto') => {
|
||||
const scrollToBottom = (behavior: ScrollBehavior = 'smooth') => {
|
||||
const el = elementRef.value;
|
||||
if (!el) return;
|
||||
el.scrollTo({
|
||||
top: el.scrollHeight,
|
||||
behavior,
|
||||
});
|
||||
shouldAutoScroll.value = true;
|
||||
};
|
||||
|
||||
const handleScroll = () => {
|
||||
const el = elementRef.value;
|
||||
if (!el) return;
|
||||
|
||||
userIsScrollingUp.value = !isAtBottom();
|
||||
const { scrollTop, scrollHeight, clientHeight } = el;
|
||||
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
|
||||
|
||||
if (distanceFromBottom <= threshold) {
|
||||
if (isUserScrollingUp.value) {
|
||||
isUserScrollingUp.value = false;
|
||||
shouldAutoScroll.value = true;
|
||||
}
|
||||
} else {
|
||||
isUserScrollingUp.value = true;
|
||||
shouldAutoScroll.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
let observer: MutationObserver | null = null;
|
||||
let timeout: NodeJS.Timeout | null = null;
|
||||
|
||||
watch(elementRef, (newEl, oldEl) => {
|
||||
if (oldEl) {
|
||||
oldEl.removeEventListener('scroll', handleScroll);
|
||||
observer?.disconnect();
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
timeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (newEl) {
|
||||
newEl.addEventListener('scroll', handleScroll, { passive: true });
|
||||
|
||||
observer = new MutationObserver(() => {
|
||||
if (!userIsScrollingUp.value) {
|
||||
scrollToBottom();
|
||||
// Only auto-scroll if user hasn't scrolled up and is near bottom
|
||||
if (!isUserScrollingUp.value && shouldAutoScroll.value) {
|
||||
scrollToBottom('instant');
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(newEl, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
characterData: true
|
||||
characterData: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -55,10 +70,13 @@ export function useAutoScroll(elementRef: Ref<HTMLElement | null>) {
|
||||
onUnmounted(() => {
|
||||
elementRef.value?.removeEventListener('scroll', handleScroll);
|
||||
observer?.disconnect();
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
userIsScrollingUp,
|
||||
scrollToBottom,
|
||||
isUserScrollingUp, // expose for UI feedback (optional)
|
||||
};
|
||||
}
|
||||
+258
-56
@@ -2,8 +2,32 @@ import type schema from "#triplit/schema";
|
||||
import type { Entity } from "@triplit/client";
|
||||
import type { ModelMessage } from "ai";
|
||||
import { decrypt, base64ToUint8Array } from "~/utils/crypto";
|
||||
import { type Result, Ok, Err } from "~~/types/result";
|
||||
import { assert } from "~~/utils/assert";
|
||||
|
||||
export type Message = Entity<typeof schema, 'messages'> & { parts: (Entity<typeof schema, 'message_parts'> & { toolCall: Entity<typeof schema, 'tool_calls'> | null } | undefined)[] };
|
||||
export type MessageEntity = Entity<typeof schema, 'messages'> & {
|
||||
parts: (Entity<typeof schema, 'message_parts'> & {
|
||||
toolCall: Entity<typeof schema, 'tool_calls'> | null
|
||||
})[] | undefined
|
||||
} & { generation: Entity<typeof schema, 'generations'> | null }
|
||||
|
||||
export type Message =
|
||||
MessageEntity & {
|
||||
children: (MessageEntity | undefined)[];
|
||||
};
|
||||
|
||||
export enum ChatErrorType {
|
||||
NoModel = 0,
|
||||
NoProvider,
|
||||
NoAgent,
|
||||
NoUser,
|
||||
DatabaseOperationFailed,
|
||||
GenerationFailed,
|
||||
MarshallFailed,
|
||||
NoProviderApiKey,
|
||||
NoMessage,
|
||||
Unimplemented,
|
||||
}
|
||||
|
||||
export const useChat = (agentId: string) => {
|
||||
const triplit = useTriplitClient();
|
||||
@@ -22,14 +46,13 @@ export const useChat = (agentId: string) => {
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
if ('flush' in triplit) {
|
||||
await triplit.flush();
|
||||
}
|
||||
assert('flush' in triplit);
|
||||
await triplit.flush();
|
||||
|
||||
return newTopic;
|
||||
};
|
||||
|
||||
const marshallMessages = (agent: Entity<typeof schema, 'agents'>, messages: Readonly<Message[]>) => {
|
||||
const marshallMessages = (agent: Entity<typeof schema, 'agents'>, messages: Readonly<MessageEntity[]>): Result<ModelMessage[], string> => {
|
||||
const marshalledMessages: ModelMessage[] = [];
|
||||
|
||||
if (agent && agent.systemPrompt) {
|
||||
@@ -49,8 +72,8 @@ export const useChat = (agentId: string) => {
|
||||
});
|
||||
break;
|
||||
case 'assistant':
|
||||
message.parts.forEach((part) => {
|
||||
if (!part) throw new Error('Part is undefined');
|
||||
(message.parts || []).forEach((part) => {
|
||||
if (!part) return Err('Part is undefined')
|
||||
|
||||
switch (part.type) {
|
||||
case 'text':
|
||||
@@ -62,22 +85,20 @@ export const useChat = (agentId: string) => {
|
||||
break;
|
||||
}
|
||||
case 'tool-call': {
|
||||
if (part.toolCall === null) throw new Error('Tool call is null');
|
||||
if (part.toolCall === null) return Err('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.',
|
||||
);
|
||||
return Err('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':
|
||||
switch (part.toolCall.input!.type) {
|
||||
case 'text':
|
||||
inputValue = part.toolCall.input!.value;
|
||||
break;
|
||||
case 'object':
|
||||
inputValue = JSON.stringify(part.toolCall.input!.value, null, 2);
|
||||
case 'json':
|
||||
inputValue = JSON.parse(part.toolCall.input!.value);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -152,39 +173,26 @@ export const useChat = (agentId: string) => {
|
||||
}
|
||||
} break;
|
||||
default:
|
||||
throw new Error(`Unknown part type: ${part.type}`);
|
||||
return Err(`Unknown part type: ${part.type}`)
|
||||
}
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown message role: ${message.role}`);
|
||||
return Err(`Unknown message role: ${message.role}`)
|
||||
}
|
||||
});
|
||||
|
||||
return marshalledMessages;
|
||||
return Ok(marshalledMessages);
|
||||
};
|
||||
|
||||
const sendMessage = async (
|
||||
message: string,
|
||||
const startGeneration = async (
|
||||
messages: ModelMessage[],
|
||||
args: Record<string, any>,
|
||||
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)
|
||||
);
|
||||
|
||||
|
||||
parentMessageId: string | null = null
|
||||
): Promise<Result<void, ChatErrorType>> => {
|
||||
let providerApiKey: string | undefined = undefined;
|
||||
if (provider.config.apiKey !== undefined) {
|
||||
const key = await crypto.subtle.importKey(
|
||||
@@ -201,32 +209,226 @@ export const useChat = (agentId: string) => {
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
try {
|
||||
$fetch('/api/chat/generate', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
messages,
|
||||
topicId: topic.id,
|
||||
parentMessageId,
|
||||
model: {
|
||||
providerId: provider.id,
|
||||
modelId: model.id,
|
||||
args,
|
||||
},
|
||||
providerApiKey: providerApiKey,
|
||||
},
|
||||
providerApiKey: providerApiKey,
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
return Ok(undefined);
|
||||
} catch (error) {
|
||||
console.error('Failed to generate:', error);
|
||||
return Err(ChatErrorType.GenerationFailed);
|
||||
}
|
||||
}
|
||||
|
||||
const sendMessage = async (
|
||||
message: string,
|
||||
topic: Entity<typeof schema, 'topics'>,
|
||||
topicMessages: MessageEntity[],
|
||||
agent: Entity<typeof schema, 'agents'>,
|
||||
provider: Entity<typeof schema, 'providers'>,
|
||||
model: Entity<typeof schema, 'models'>,
|
||||
): Promise<Result<void, ChatErrorType>> => {
|
||||
const { user } = useAuth();
|
||||
if (!user.value) {
|
||||
console.error('No user');
|
||||
return Err(ChatErrorType.NoUser);
|
||||
}
|
||||
|
||||
const newMessage = await triplit.insert('messages', {
|
||||
userId: user.value.id,
|
||||
topicId: topic.id,
|
||||
createdAt: new Date().toISOString(),
|
||||
content: message,
|
||||
role: 'user',
|
||||
}).catch(error => {
|
||||
console.error('Failed to insert message:', error);
|
||||
return Err(ChatErrorType.DatabaseOperationFailed);
|
||||
}) as Message;
|
||||
|
||||
const messages = marshallMessages(
|
||||
agent,
|
||||
topicMessages.concat(newMessage)
|
||||
);
|
||||
|
||||
if (messages.ok === false) {
|
||||
console.error('Failed to marshall messages:', messages.error);
|
||||
return Err(ChatErrorType.MarshallFailed)
|
||||
}
|
||||
|
||||
const args = {
|
||||
temperature: 1,
|
||||
max_tokens: 100,
|
||||
top_p: 1,
|
||||
frequency_penalty: 0,
|
||||
presence_penalty: 0,
|
||||
};
|
||||
|
||||
return startGeneration(messages.data, args, topic, provider, model)
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param messageId The ID of the message we wish to regenerate *for*, this
|
||||
* can be either a user's message or an agent's message, and we will find
|
||||
* the message that should be regenerated automatically
|
||||
* @param topic
|
||||
* @param topicMessages messages in the current topic, if this does not
|
||||
* include an agent message after the user's message we select, the new
|
||||
* message will not be a child of the previous message. However, if this
|
||||
* array contains an agent message that is to be regenerated, the new
|
||||
* message will have that message's id as its parent. The message
|
||||
* reference by messageId *must* be in this array
|
||||
* @param agent
|
||||
* @param provider
|
||||
* @param model
|
||||
*/
|
||||
const regenerateMessage = async (
|
||||
messageId: string,
|
||||
topic: Entity<typeof schema, 'topics'>,
|
||||
topicMessages: MessageEntity[],
|
||||
agent: Entity<typeof schema, 'agents'>,
|
||||
provider: Entity<typeof schema, 'providers'>,
|
||||
model: Entity<typeof schema, 'models'>
|
||||
): Promise<Result<void, ChatErrorType>> => {
|
||||
const targetMessage = topicMessages.find(message => message.id === messageId);
|
||||
if (!targetMessage) {
|
||||
return Err(ChatErrorType.NoMessage);
|
||||
}
|
||||
let targetMessageIndex = topicMessages.indexOf(targetMessage);
|
||||
|
||||
const args = {
|
||||
temperature: 1,
|
||||
max_tokens: 100,
|
||||
top_p: 1,
|
||||
frequency_penalty: 0,
|
||||
presence_penalty: 0,
|
||||
};
|
||||
|
||||
let parentMessageId = null;
|
||||
let focusedMessages;
|
||||
if (targetMessage.role === 'user') {
|
||||
// we need to find the next agent message
|
||||
while (targetMessageIndex < topicMessages.length) {
|
||||
const currentMessage = topicMessages[targetMessageIndex];
|
||||
if (!currentMessage) break;
|
||||
|
||||
if (currentMessage.role === 'assistant') {
|
||||
parentMessageId = currentMessage.parentMessageId || currentMessage.id;
|
||||
focusedMessages = topicMessages.slice(0, targetMessageIndex).filter(message => message.id !== currentMessage.id);
|
||||
break;
|
||||
}
|
||||
targetMessageIndex++;
|
||||
}
|
||||
} else {
|
||||
parentMessageId = targetMessage.parentMessageId || topicMessages[targetMessageIndex]!.id;
|
||||
focusedMessages = topicMessages.slice(0, targetMessageIndex).filter(message => message.id !== messageId);
|
||||
}
|
||||
|
||||
if (focusedMessages === undefined) {
|
||||
focusedMessages = topicMessages;
|
||||
}
|
||||
|
||||
if (parentMessageId === null) {
|
||||
const messages = marshallMessages(
|
||||
agent,
|
||||
topicMessages
|
||||
);
|
||||
if (messages.ok === false) {
|
||||
console.error('Failed to marshall messages:', messages.error);
|
||||
return Err(ChatErrorType.MarshallFailed)
|
||||
}
|
||||
|
||||
return startGeneration(messages.data, args, topic, provider, model);
|
||||
}
|
||||
|
||||
const focusedMessageIndex = topicMessages.findIndex(m => m.id === parentMessageId);
|
||||
if (focusedMessageIndex !== topicMessages.length - 1) {
|
||||
|
||||
}
|
||||
|
||||
const messages = marshallMessages(
|
||||
agent,
|
||||
focusedMessages
|
||||
);
|
||||
if (messages.ok === false) {
|
||||
console.error('Failed to marshall messages:', messages.error);
|
||||
return Err(ChatErrorType.MarshallFailed)
|
||||
}
|
||||
|
||||
return startGeneration(messages.data, args, topic, provider, model, parentMessageId);
|
||||
}
|
||||
|
||||
const autoRename = async (topicId: string, prompt: string) => {
|
||||
const { settings } = await useUserSettings();
|
||||
|
||||
console.log(settings.value);
|
||||
|
||||
if (!settings.value.systemAssistants.rename.enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!settings.value.systemAssistants.rename.modelId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await triplit.update('topics', topicId, {
|
||||
renaming: true
|
||||
});
|
||||
|
||||
const model = await triplit.fetchOne(triplit.query('models').Where('id', '=', settings.value.systemAssistants.rename.modelId).Include('provider'));
|
||||
|
||||
if (!model) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let providerApiKey: string | undefined = undefined;
|
||||
if (model.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(model.provider!.config.apiKey)
|
||||
);
|
||||
}
|
||||
|
||||
await $fetch(`/api/topic/auto-rename`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
modelId: model.id,
|
||||
topicId,
|
||||
prompt,
|
||||
providerApiKey,
|
||||
}),
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return {
|
||||
sendMessage,
|
||||
autoRename,
|
||||
regenerateMessage,
|
||||
createTopic,
|
||||
};
|
||||
}
|
||||
@@ -10,42 +10,57 @@ export type ProviderWithModels = Entity<typeof schema, 'providers'> & {
|
||||
};
|
||||
|
||||
export const useModels = async () => {
|
||||
const triplit = useTriplitClient();
|
||||
const nuxtApp = useNuxtApp() as any;
|
||||
|
||||
const providersQuery = triplit
|
||||
.query('providers')
|
||||
.Include('models')
|
||||
if (!nuxtApp._modelsSubscription) {
|
||||
if (!nuxtApp._modelsPromise) {
|
||||
const triplit = useTriplitClient();
|
||||
|
||||
const { results: providers, unsubscribe } = await useQuery('providers', triplit, providersQuery);
|
||||
const providersQuery = triplit
|
||||
.query('providers')
|
||||
.Include('models');
|
||||
|
||||
console.log("GET PROVIDERS", providers.value);
|
||||
nuxtApp._modelsPromise = useQuery('providers', triplit, providersQuery).then((sub) => {
|
||||
nuxtApp._modelsSubscription = sub;
|
||||
return sub;
|
||||
});
|
||||
}
|
||||
await nuxtApp._modelsPromise;
|
||||
}
|
||||
|
||||
// const enabledProvidersWithModels = computed<ProviderWithModels[]>(() => {
|
||||
// if (!providers.value) return [];
|
||||
const { results: providers } = nuxtApp._modelsSubscription;
|
||||
|
||||
// return (providers.value as unknown as ProviderWithModels[]).filter(
|
||||
// (provider: ProviderWithModels) => provider.models && provider.models.length > 0
|
||||
// );
|
||||
// });
|
||||
if (!nuxtApp._allModels) {
|
||||
nuxtApp._allModels = computed<ModelWithProvider[]>(() => {
|
||||
if (!providers.value) return [];
|
||||
|
||||
const allEnabledModels = computed<ModelWithProvider[]>(() => {
|
||||
return providers.value?.flatMap((provider) =>
|
||||
provider.models.map((model) => ({
|
||||
...model,
|
||||
provider,
|
||||
}))
|
||||
);
|
||||
});
|
||||
const list: ModelWithProvider[] = [];
|
||||
|
||||
for (const provider of (providers.value as ProviderWithModels[])) {
|
||||
if (!provider.enabled) continue;
|
||||
|
||||
for (const model of provider.models) {
|
||||
if (!model.enabled) continue;
|
||||
|
||||
list.push({
|
||||
...model,
|
||||
provider
|
||||
} as unknown as ModelWithProvider);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
});
|
||||
}
|
||||
|
||||
const getFirstAvailableModel = (): ModelWithProvider | null => {
|
||||
if (allEnabledModels.value.length === 0) return null;
|
||||
return allEnabledModels.value[0]!;
|
||||
if (nuxtApp._allModels.value.length === 0) return null;
|
||||
return nuxtApp._allModels.value[0]!;
|
||||
};
|
||||
|
||||
return {
|
||||
providers,
|
||||
allModels: allEnabledModels,
|
||||
providers: providers as Ref<ProviderWithModels[]>,
|
||||
allModels: nuxtApp._allModels as ComputedRef<ModelWithProvider[]>,
|
||||
getFirstAvailableModel,
|
||||
unsubscribe,
|
||||
unsubscribe: () => { },
|
||||
};
|
||||
};
|
||||
|
||||
@@ -8,6 +8,8 @@ export const useSettings = () => {
|
||||
};
|
||||
|
||||
const setPage = (id: string, params?: string | string[]) => {
|
||||
if (open.value === false) toggle();
|
||||
|
||||
currentPage.value = id;
|
||||
if (params) {
|
||||
if (typeof params === 'string') {
|
||||
|
||||
@@ -38,7 +38,7 @@ export const useSidebar = () => {
|
||||
};
|
||||
|
||||
return {
|
||||
open,
|
||||
open: readonly(open),
|
||||
toggle,
|
||||
close,
|
||||
openSidebar,
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
const SIDENAV_CONTEXT_KEY = Symbol('sidenav-context');
|
||||
|
||||
export interface SidenavContext {
|
||||
isHovered: Readonly<Ref<boolean>>;
|
||||
sidebarWidth: Readonly<Ref<number>>;
|
||||
isOpen: Readonly<Ref<boolean>>;
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
export const provideSidenavContext = (context: SidenavContext) => {
|
||||
provide(SIDENAV_CONTEXT_KEY, context);
|
||||
};
|
||||
|
||||
export const useSidenavContext = (): SidenavContext => {
|
||||
const context = inject<SidenavContext>(SIDENAV_CONTEXT_KEY);
|
||||
if (!context) {
|
||||
throw new Error('useSidenavContext must be used within a Sidenav provider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
export const useUserSettings = async () => {
|
||||
const user = useAuth().user;
|
||||
const triplit = useTriplitClient();
|
||||
|
||||
const { results: settings, unsubscribe } = await useQuery('settings', triplit, triplit.query('settings').Where('userId', '=', user.value!.id));
|
||||
|
||||
return { settings: computed(() => settings.value![0]!), unsubscribe };
|
||||
}
|
||||
Reference in New Issue
Block a user