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
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>
|
|
|