feat: per-user assistant name and system prompt customization
Each user can rename the AI assistant and edit their own system prompt in Settings.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { z } from "zod";
|
||||
import { apiError, apiJson } from "@/lib/api-handler";
|
||||
import { resolveApiAuth } from "@/lib/api-auth";
|
||||
import { getAssistantEnabled } from "@/lib/assistant-preference";
|
||||
import { getAssistantPreferences, resolveAssistantSystemPrompt } from "@/lib/assistant-preference";
|
||||
import { isLlmConfigured } from "@/lib/llm";
|
||||
import { encodeSseEvent } from "@/modules/agent/server/progress";
|
||||
import { runAgentChat } from "@/modules/agent/server/run";
|
||||
@@ -25,11 +25,13 @@ export async function POST(request: Request) {
|
||||
return apiError("Unauthorized", 401);
|
||||
}
|
||||
|
||||
const assistantEnabled = await getAssistantEnabled(auth.userId);
|
||||
if (!assistantEnabled) {
|
||||
const assistant = await getAssistantPreferences(auth.userId);
|
||||
if (!assistant.enabled) {
|
||||
return apiError("Assistant not enabled", 403);
|
||||
}
|
||||
|
||||
const systemPrompt = resolveAssistantSystemPrompt(assistant.systemPrompt);
|
||||
|
||||
let body: unknown;
|
||||
try {
|
||||
body = await request.json();
|
||||
@@ -54,6 +56,7 @@ export async function POST(request: Request) {
|
||||
const result = await runAgentChat({
|
||||
messages: parsed.data.messages,
|
||||
request,
|
||||
systemPrompt,
|
||||
onProgress: send,
|
||||
});
|
||||
|
||||
@@ -87,6 +90,7 @@ export async function POST(request: Request) {
|
||||
const result = await runAgentChat({
|
||||
messages: parsed.data.messages,
|
||||
request,
|
||||
systemPrompt,
|
||||
});
|
||||
|
||||
return apiJson({
|
||||
|
||||
+9
-1
@@ -18,6 +18,7 @@ import { InstallPrompt } from "@/components/install-prompt";
|
||||
import { AppShell } from "@/components/app-shell";
|
||||
import { AppToaster } from "@/components/app-toaster";
|
||||
import { AssistantBubble } from "@/modules/agent/components/assistant-bubble";
|
||||
import { DEFAULT_ASSISTANT_NAME } from "@/lib/assistant-config";
|
||||
import { isLlmConfigured } from "@/lib/llm";
|
||||
import { DEFAULT_THEME, navStyleToDataNav } from "@/modules/_core/themes";
|
||||
import type { Palette, ThemeMode, FontPair, Density, NavStyle } from "@/modules/_core/themes";
|
||||
@@ -102,6 +103,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
||||
let userDashboards: DashboardMeta[] = [];
|
||||
let signedIn = false;
|
||||
let assistantEnabled = false;
|
||||
let assistantName = DEFAULT_ASSISTANT_NAME;
|
||||
|
||||
const session = await auth();
|
||||
if (session?.user?.id) {
|
||||
@@ -114,6 +116,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
||||
themeDensity: users.themeDensity,
|
||||
themeNavStyle: users.themeNavStyle,
|
||||
assistantEnabled: users.assistantEnabled,
|
||||
assistantName: users.assistantName,
|
||||
})
|
||||
.from(users)
|
||||
.where(eq(users.id, session.user.id))
|
||||
@@ -125,6 +128,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
||||
density = row.themeDensity as Density;
|
||||
navStyle = row.themeNavStyle as NavStyle;
|
||||
assistantEnabled = row.assistantEnabled;
|
||||
assistantName = row.assistantName?.trim() || DEFAULT_ASSISTANT_NAME;
|
||||
}
|
||||
userDashboards = await db
|
||||
.select({
|
||||
@@ -178,7 +182,11 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
||||
<InstallPrompt />
|
||||
<PwaRegister />
|
||||
{signedIn && assistantEnabled && session?.user?.id ? (
|
||||
<AssistantBubble configured={isLlmConfigured()} userId={session.user.id} />
|
||||
<AssistantBubble
|
||||
configured={isLlmConfigured()}
|
||||
userId={session.user.id}
|
||||
assistantName={assistantName}
|
||||
/>
|
||||
) : null}
|
||||
<AppToaster position="bottom-right" />
|
||||
</QuickAddProvider>
|
||||
|
||||
@@ -2,13 +2,58 @@
|
||||
|
||||
import { eq } from "drizzle-orm";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { z } from "zod";
|
||||
import { db } from "@/lib/db";
|
||||
import {
|
||||
DEFAULT_ASSISTANT_NAME,
|
||||
MAX_ASSISTANT_NAME_LENGTH,
|
||||
MAX_ASSISTANT_SYSTEM_PROMPT_LENGTH,
|
||||
} from "@/lib/assistant-config";
|
||||
import { users } from "@/modules/_core/schema";
|
||||
import { getCurrentSession } from "@/lib/session";
|
||||
|
||||
const assistantNameSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Name is required")
|
||||
.max(MAX_ASSISTANT_NAME_LENGTH);
|
||||
|
||||
const assistantSystemPromptSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Prompt cannot be empty")
|
||||
.max(MAX_ASSISTANT_SYSTEM_PROMPT_LENGTH);
|
||||
|
||||
function revalidateAssistantSurfaces() {
|
||||
revalidatePath("/settings");
|
||||
revalidatePath("/", "layout");
|
||||
}
|
||||
|
||||
export async function setAssistantEnabled(enabled: boolean): Promise<void> {
|
||||
const { user } = await getCurrentSession();
|
||||
await db.update(users).set({ assistantEnabled: enabled }).where(eq(users.id, user.id));
|
||||
revalidatePath("/settings");
|
||||
revalidatePath("/", "layout");
|
||||
revalidateAssistantSurfaces();
|
||||
}
|
||||
|
||||
export async function setAssistantName(name: string): Promise<void> {
|
||||
const parsed = assistantNameSchema.parse(name);
|
||||
const { user } = await getCurrentSession();
|
||||
await db.update(users).set({ assistantName: parsed }).where(eq(users.id, user.id));
|
||||
revalidateAssistantSurfaces();
|
||||
}
|
||||
|
||||
export async function setAssistantSystemPrompt(prompt: string | null): Promise<void> {
|
||||
const { user } = await getCurrentSession();
|
||||
const normalized = prompt === null ? null : assistantSystemPromptSchema.parse(prompt);
|
||||
|
||||
await db.update(users).set({ assistantSystemPrompt: normalized }).where(eq(users.id, user.id));
|
||||
revalidateAssistantSurfaces();
|
||||
}
|
||||
|
||||
export async function resetAssistantName(): Promise<void> {
|
||||
await setAssistantName(DEFAULT_ASSISTANT_NAME);
|
||||
}
|
||||
|
||||
export async function resetAssistantSystemPrompt(): Promise<void> {
|
||||
await setAssistantSystemPrompt(null);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,8 @@ import { AvatarFallbackWithName } from "@/components/avatar-fallback";
|
||||
import { revokeShareLinkAction } from "./actions";
|
||||
import { getHouseholdApiTokenStatus } from "@/modules/_core/api-token";
|
||||
import { ApiTokenSettings } from "@/components/api-token-settings";
|
||||
import { AssistantOptIn } from "@/components/assistant-opt-in";
|
||||
import { AssistantSettings } from "@/components/assistant-settings";
|
||||
import { AGENT_SYSTEM_PROMPT } from "@/modules/agent/tools";
|
||||
import { DefaultEventRemindersSetting } from "@/components/default-event-reminders-setting";
|
||||
import { listCalendars } from "@/modules/calendar/server/queries";
|
||||
import { listLists } from "@/modules/lists/server/queries";
|
||||
@@ -322,6 +323,8 @@ function AppearanceSection({
|
||||
themeCalView: string;
|
||||
themeNavStyle: string;
|
||||
assistantEnabled: boolean;
|
||||
assistantName: string;
|
||||
assistantSystemPrompt: string | null;
|
||||
};
|
||||
}) {
|
||||
return (
|
||||
@@ -355,7 +358,12 @@ function AppearanceSection({
|
||||
<MessageCircle className="size-4 text-[var(--ink-mute)]" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<AssistantOptIn enabled={user.assistantEnabled} />
|
||||
<AssistantSettings
|
||||
enabled={user.assistantEnabled}
|
||||
name={user.assistantName}
|
||||
systemPrompt={user.assistantSystemPrompt}
|
||||
defaultSystemPrompt={AGENT_SYSTEM_PROMPT}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user