You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

269 lines
8.0 KiB

<script setup lang="ts">
import { useAgentSessions } from "~/composables/useAgentSessions";
import { useAgentChat } from "~/composables/useAgentChat";
import { useAuthSession } from "~/composables/useAuthSession";
definePageMeta({
layout: "agent",
});
const auth = useAuthSession();
const sessions = useAgentSessions();
const isMobile = useMediaQuery('(max-width: 767.98px)');
const sidebarCollapsed = ref(false);
const mobileSidebarOpen = ref(false);
const showPromptModal = ref(false);
const systemPromptText = ref("");
const preferredModelId = ref<number | null>(null);
const guestDefaultModelId = ref<number | null>(null);
interface ModelOption {
id: number;
name: string;
modelId: string;
providerName: string;
supportsTools: number;
}
const models = ref<ModelOption[]>([]);
const currentModelId = computed({
get: () => {
const sessionModelId = sessions.currentSession.value?.modelId ?? null;
if (sessionModelId !== null && models.value.length > 0 && !models.value.some((m) => m.id === sessionModelId)) {
return null;
}
return sessionModelId;
},
set: (val: number | null) => {
if (sessions.currentSessionId.value) {
sessions.updateSessionConfig(sessions.currentSessionId.value, { modelId: val });
}
},
});
watch(
() => [sessions.currentSessionId.value, models.value] as const,
([sid, modelList]) => {
if (!sid || modelList.length === 0) return;
const session = sessions.currentSession.value;
const mid = session?.modelId ?? null;
if (mid !== null && !modelList.some((m) => m.id === mid)) {
currentModelId.value = null;
} else if (mid === null) {
const fallbackSource = auth.loggedIn.value
? (preferredModelId.value && modelList.some((m) => m.id === preferredModelId.value) ? preferredModelId.value : null)
: (guestDefaultModelId.value && modelList.some((m) => m.id === guestDefaultModelId.value) ? guestDefaultModelId.value : null);
const fallback = fallbackSource ?? modelList[0].id;
currentModelId.value = fallback;
}
},
);
const currentModelSupportsTools = computed(() => {
const mid = currentModelId.value;
if (mid === null) return true;
const model = models.value.find((m) => m.id === mid);
return model ? model.supportsTools === 1 : true;
});
const enableTools = computed({
get: () => {
if (!currentModelSupportsTools.value) return false;
return !!(sessions.currentSession.value?.enableTools);
},
set: (val: boolean) => {
if (!currentModelSupportsTools.value) return;
if (sessions.currentSessionId.value) {
sessions.updateSessionConfig(sessions.currentSessionId.value, { enableTools: val });
}
},
});
const chat = useAgentChat({
sessionId: () => sessions.currentSessionId.value,
modelId: () => currentModelId.value,
enableThinking: () => false,
enableTools: () => enableTools.value,
onStreamComplete: () => {
const sid = sessions.currentSessionId.value;
if (!sid) return;
setTimeout(() => sessions.refreshSessionTitle(sid), 3000);
},
});
const rateLimitInfo = computed(() => {
if (auth.loggedIn.value) return undefined;
return {
sessionRemaining: chat.rateLimit.sessionRemaining.value,
ipRemaining: chat.rateLimit.ipRemaining.value,
};
});
async function loadModels() {
try {
const isGuest = !auth.loggedIn.value;
const endpoint = isGuest ? "/api/agent/models" : "/api/llm/models";
const res = await $fetch<{ code: number; data: { list: any[]; defaultModelId?: number | null } }>(endpoint);
const list = res.data?.list ?? [];
models.value = list.map((m) => ({
id: m.id,
name: m.name,
modelId: m.modelId,
providerName: m.providerName ?? "—",
supportsTools: m.supportsTools ?? 1,
}));
if (isGuest) {
guestDefaultModelId.value = res.data?.defaultModelId ?? null;
}
} catch {
models.value = [];
}
}
async function loadUserConfig() {
if (!auth.loggedIn.value) return;
try {
const res = await $fetch<{ code: number; data: { config: Record<string, unknown> } }>(
"/api/config/me",
);
const val = res.data?.config?.preferredLlmModelId;
preferredModelId.value = typeof val === "number" && val > 0 ? val : null;
} catch {
preferredModelId.value = null;
}
}
async function loadSystemPrompt() {
try {
const res = await $fetch<{ code: number; data: { systemPrompt?: string } }>(
"/api/agent/system-prompt",
);
systemPromptText.value = res.data?.systemPrompt ?? "";
} catch {
systemPromptText.value = "";
}
}
watch(
() => sessions.currentSessionId.value,
async (sid) => {
if (sid) {
await chat.loadMessages(sid);
} else {
chat.clear();
}
},
);
watch(isMobile, (mobile) => {
if (mobile) {
mobileSidebarOpen.value = false;
} else {
sidebarCollapsed.value = false;
}
}, { immediate: true });
async function handleSend(content: string) {
if (!sessions.currentSessionId.value) {
await sessions.newSession();
}
if (sessions.currentSessionId.value) {
const sid = sessions.currentSessionId.value;
const wasNewSession = chat.messages.value.filter((m) => m.role === "assistant").length === 0;
await chat.send(content);
sessions.touchSessionOrder(sid);
if (wasNewSession) {
refreshTitleWithRetry(sid);
}
}
}
function refreshTitleWithRetry(sid: string) {
const delays = [3000, 6000, 10000, 15000, 22000];
delays.forEach((delay) => {
setTimeout(() => sessions.refreshSessionTitle(sid), delay);
});
}
async function handleApprove(toolCallId: string, approved: boolean) {
await chat.respondToApproval(toolCallId, approved);
}
async function handleFeedback(messageId: string, feedback: "like" | "dislike") {
await chat.sendFeedback(messageId, feedback);
}
function handleEdit(messageId: string, content: string) {
chat.send(content, { editMessageId: messageId });
}
function handleRegenerate() {
const lastUserMsg = [...chat.messages.value].reverse().find((m) => m.role === "user");
if (lastUserMsg) {
chat.send(lastUserMsg.content, { regenerate: true });
}
}
function handleExample(content: string) {
handleSend(content);
}
function handleLogin() {
navigateTo("/auth/login");
}
onMounted(async () => {
await Promise.all([sessions.init(), loadModels(), loadSystemPrompt(), loadUserConfig()]);
});
</script>
<template>
<AgentSidebar
:sessions="sessions.sessions.value"
:current-session-id="sessions.currentSessionId.value"
:logged-in="auth.loggedIn.value"
:user="auth.user.value"
:collapsed="isMobile ? !mobileSidebarOpen : sidebarCollapsed"
:mobile="isMobile"
@new-session="sessions.newSession(); if (isMobile) mobileSidebarOpen = false"
@select="sessions.selectSession($event); if (isMobile) mobileSidebarOpen = false"
@rename="sessions.renameSession"
@delete="sessions.deleteSession"
@login="handleLogin"
@toggle="isMobile ? (mobileSidebarOpen = !mobileSidebarOpen) : (sidebarCollapsed = !sidebarCollapsed)"
@close="mobileSidebarOpen = false"
/>
<AgentChatArea
:messages="chat.messages.value"
:is-loading="chat.isLoading.value"
:has-pending-approval="chat.hasPendingApproval.value"
:logged-in="auth.loggedIn.value"
:model-id="currentModelId"
:models="models"
:enable-tools="enableTools"
:tools-disabled="!currentModelSupportsTools"
:rate-limit-info="rateLimitInfo"
:sidebar-collapsed="sidebarCollapsed"
:error-message="chat.errorMessage.value"
:system-prompt="systemPromptText"
@send="handleSend"
@stop="chat.stopGeneration()"
@update:model-id="currentModelId = $event"
@update:enable-tools="enableTools = $event"
@approve="handleApprove"
@feedback="handleFeedback"
@edit="handleEdit"
@regenerate="handleRegenerate"
@toggle-sidebar="isMobile ? (mobileSidebarOpen = !mobileSidebarOpen) : (sidebarCollapsed = !sidebarCollapsed)"
@show-prompt="showPromptModal = true"
@example="handleExample"
/>
<AgentSystemPromptModal
:show="showPromptModal"
:system-prompt="systemPromptText"
@close="showPromptModal = false"
/>
</template>