Add per-chat settings UI in web app for additional system prompt and tool checkboxes

This commit is contained in:
Agent
2026-05-24 22:04:05 +00:00
committed by James Magahern
parent 0bf0f95a67
commit 4a2493c421
11 changed files with 321 additions and 32 deletions

View File

@@ -1,5 +1,22 @@
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
import { Check, ChevronDown, Globe2, LoaderCircle, Menu, MessageSquare, Paperclip, Pencil, Plus, Rabbit, Search, SendHorizontal, Star, Trash2, X } from "lucide-preact";
import {
Check,
ChevronDown,
Globe2,
LoaderCircle,
Menu,
MessageSquare,
Paperclip,
Pencil,
Plus,
Rabbit,
Search,
SendHorizontal,
Settings2,
Star,
Trash2,
X,
} from "lucide-preact";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Separator } from "@/components/ui/separator";
@@ -18,6 +35,7 @@ import {
attachSearchStream,
getActiveRuns,
getChat,
listChatTools,
listModels,
getSearch,
listWorkspaceItems,
@@ -27,6 +45,7 @@ import {
updateChatTitle,
updateChatStar,
updateSearchStar,
updateChatSettings,
getMessageAttachments,
type ChatAttachment,
type ActiveRunsResponse,
@@ -34,6 +53,7 @@ import {
type Provider,
type ChatDetail,
type ChatSummary,
type ChatToolInfo,
type CompletionRequestMessage,
type Message,
type SearchDetail,
@@ -379,6 +399,30 @@ function getProviderLabel(provider: Provider | null | undefined) {
return "";
}
function getToolLabel(name: string) {
if (name === "web_search") return "Web search";
if (name === "fetch_url") return "Fetch URL";
if (name === "codex_exec") return "Codex";
if (name === "shell_exec") return "Shell";
return name
.split("_")
.filter(Boolean)
.map((part) => part.slice(0, 1).toUpperCase() + part.slice(1))
.join(" ");
}
function getDefaultEnabledTools(availableTools: ChatToolInfo[]) {
return availableTools.map((tool) => tool.name);
}
function normalizeEnabledTools(value: unknown, availableTools: ChatToolInfo[]) {
const available = new Set(availableTools.map((tool) => tool.name));
if (!Array.isArray(value)) return getDefaultEnabledTools(availableTools);
return [...new Set(value.filter((item): item is string => typeof item === "string").map((item) => item.trim()).filter(Boolean))].filter((name) =>
available.has(name)
);
}
function getChatModelSelection(chat: Pick<ChatSummary, "lastUsedProvider" | "lastUsedModel"> | Pick<ChatDetail, "lastUsedProvider" | "lastUsedModel"> | null) {
if (!chat?.lastUsedProvider || !chat.lastUsedModel?.trim()) return null;
return {
@@ -748,6 +792,7 @@ export default function App() {
const [isComposerDropActive, setIsComposerDropActive] = useState(false);
const [provider, setProvider] = useState<Provider>("openai");
const [modelCatalog, setModelCatalog] = useState<ModelCatalogResponse["providers"]>(EMPTY_MODEL_CATALOG);
const [availableChatTools, setAvailableChatTools] = useState<ChatToolInfo[]>([]);
const [providerModelPreferences, setProviderModelPreferences] = useState<ProviderModelPreferences>(() => loadStoredModelPreferences());
const [model, setModel] = useState(() => {
const stored = loadStoredModelPreferences();
@@ -774,6 +819,9 @@ export default function App() {
const [renameChatDraft, setRenameChatDraft] = useState("");
const [renameChatError, setRenameChatError] = useState<string | null>(null);
const [isRenamingChat, setIsRenamingChat] = useState(false);
const [isChatSettingsOpen, setIsChatSettingsOpen] = useState(false);
const [additionalSystemPrompt, setAdditionalSystemPrompt] = useState("");
const [enabledTools, setEnabledTools] = useState<string[]>([]);
const [transcriptTailSpacerHeight, setTranscriptTailSpacerHeight] = useState(TRANSCRIPT_BOTTOM_GAP);
const transcriptContainerRef = useRef<HTMLDivElement>(null);
const transcriptEndRef = useRef<HTMLDivElement>(null);
@@ -899,6 +947,9 @@ export default function App() {
searchRunCountersRef.current.clear();
setComposer("");
setPendingAttachments([]);
setIsChatSettingsOpen(false);
setAdditionalSystemPrompt("");
setEnabledTools([]);
setIsQuickQuestionOpen(false);
setQuickPrompt("");
setQuickSubmittedPrompt(null);
@@ -968,6 +1019,21 @@ export default function App() {
}
};
const refreshChatTools = async () => {
try {
const tools = await listChatTools();
setAvailableChatTools(tools);
setEnabledTools((current) => normalizeEnabledTools(current.length ? current : null, tools));
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
if (message.includes("bearer token")) {
handleAuthFailure(message);
} else {
setError(message);
}
}
};
const refreshActiveRuns = async () => {
try {
const data = await getActiveRuns();
@@ -1020,7 +1086,7 @@ export default function App() {
if (!isAuthenticated) return;
const preferredSelection = initialRouteSelectionRef.current;
initialRouteSelectionRef.current = null;
void Promise.all([refreshCollections(preferredSelection ?? undefined), refreshModels(), refreshActiveRuns()]);
void Promise.all([refreshCollections(preferredSelection ?? undefined), refreshModels(), refreshChatTools(), refreshActiveRuns()]);
}, [isAuthenticated]);
useEffect(() => {
@@ -1287,6 +1353,19 @@ export default function App() {
setModel(nextSelection.model);
}, [draftKind, selectedChat, selectedChatSummary, selectedItem]);
useEffect(() => {
if (draftKind === "chat") {
setAdditionalSystemPrompt("");
setEnabledTools(getDefaultEnabledTools(availableChatTools));
return;
}
if (selectedItem?.kind !== "chat") return;
const chat = selectedChat?.id === selectedItem.id ? selectedChat : selectedChatSummary;
if (!chat) return;
setAdditionalSystemPrompt(chat.additionalSystemPrompt ?? "");
setEnabledTools(normalizeEnabledTools(chat.enabledTools, availableChatTools));
}, [availableChatTools, draftKind, selectedChat, selectedChatSummary, selectedItem]);
const selectedTitle = useMemo(() => {
if (draftKind === "chat") return "New chat";
if (draftKind === "search") return "New search";
@@ -1441,6 +1520,8 @@ export default function App() {
initiatedModel: updatedChat.initiatedModel,
lastUsedProvider: updatedChat.lastUsedProvider,
lastUsedModel: updatedChat.lastUsedModel,
additionalSystemPrompt: updatedChat.additionalSystemPrompt,
enabledTools: updatedChat.enabledTools,
};
});
};
@@ -1768,6 +1849,8 @@ export default function App() {
initiatedModel: chat.initiatedModel,
lastUsedProvider: chat.lastUsedProvider,
lastUsedModel: chat.lastUsedModel,
additionalSystemPrompt: chat.additionalSystemPrompt,
enabledTools: chat.enabledTools,
messages: [],
});
setSelectedSearch(null);
@@ -2349,6 +2432,8 @@ export default function App() {
initiatedModel: chat.initiatedModel,
lastUsedProvider: chat.lastUsedProvider,
lastUsedModel: chat.lastUsedModel,
additionalSystemPrompt: chat.additionalSystemPrompt,
enabledTools: chat.enabledTools,
messages: [],
});
setSelectedSearch(null);
@@ -2527,6 +2612,8 @@ export default function App() {
initiatedModel: chat.initiatedModel,
lastUsedProvider: chat.lastUsedProvider,
lastUsedModel: chat.lastUsedModel,
additionalSystemPrompt: chat.additionalSystemPrompt,
enabledTools: chat.enabledTools,
messages: [],
});
setSelectedSearch(null);

View File

@@ -9,6 +9,8 @@ export type ChatSummary = {
initiatedModel: string | null;
lastUsedProvider: Provider | null;
lastUsedModel: string | null;
additionalSystemPrompt: string | null;
enabledTools: string[] | null;
};
export type SearchSummary = {
@@ -64,6 +66,8 @@ export type ChatDetail = {
initiatedModel: string | null;
lastUsedProvider: Provider | null;
lastUsedModel: string | null;
additionalSystemPrompt: string | null;
enabledTools: string[] | null;
messages: Message[];
};
@@ -157,6 +161,11 @@ export type ModelCatalogResponse = {
providers: Partial<Record<Provider, ProviderModelInfo>>;
};
export type ChatToolInfo = {
name: string;
description: string;
};
export type ActiveRunsResponse = {
chats: string[];
searches: string[];
@@ -182,6 +191,8 @@ type CreateChatRequest = {
title?: string;
provider?: Provider;
model?: string;
additionalSystemPrompt?: string;
enabledTools?: string[];
messages?: CompletionRequestMessage[];
};
@@ -257,6 +268,11 @@ export async function listModels() {
return api<ModelCatalogResponse>("/v1/models");
}
export async function listChatTools() {
const data = await api<{ tools: ChatToolInfo[] }>("/v1/chat-tools");
return data.tools;
}
export async function getActiveRuns() {
return api<ActiveRunsResponse>("/v1/active-runs");
}
@@ -291,6 +307,14 @@ export async function updateChatStar(chatId: string, starred: boolean) {
return data.chat;
}
export async function updateChatSettings(chatId: string, body: { additionalSystemPrompt?: string | null; enabledTools?: string[] }) {
const data = await api<{ chat: ChatSummary }>(`/v1/chats/${chatId}`, {
method: "PATCH",
body: JSON.stringify(body),
});
return data.chat;
}
export async function suggestChatTitle(body: { chatId: string; content: string }) {
const data = await api<{ chat: ChatSummary }>("/v1/chats/title/suggest", {
method: "POST",
@@ -613,6 +637,8 @@ export async function runCompletion(body: {
provider: Provider;
model: string;
messages: CompletionRequestMessage[];
additionalSystemPrompt?: string;
enabledTools?: string[];
userLocation?: string;
}) {
return api<CompletionResponse>("/v1/chat-completions", {
@@ -628,6 +654,8 @@ export async function runCompletionStream(
provider: Provider;
model: string;
messages: CompletionRequestMessage[];
additionalSystemPrompt?: string;
enabledTools?: string[];
userLocation?: string;
},
handlers: CompletionStreamHandlers,