Add per-chat settings UI in web app for additional system prompt and tool checkboxes
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user