From 377d9ec8d80c0d2f6b7cdde1d2b1b1e65793718e Mon Sep 17 00:00:00 2001 From: ComputelessComputer Date: Sat, 11 Apr 2026 12:26:18 +0900 Subject: [PATCH] fix: add clean personalization settings flow Surface chat personalization in Settings, wire the stored values into the chat system prompt, and rename the remaining meeting-note and pricing copy. --- apps/desktop/src/ai/prompts/details.tsx | 4 +- apps/desktop/src/ai/prompts/index.tsx | 2 +- apps/desktop/src/ai/prompts/list.tsx | 2 +- .../src/chat/transport/use-transport.ts | 86 ++--- apps/desktop/src/settings/index.tsx | 3 + .../src/settings/personalization/index.tsx | 303 ++++++++++++++++++ apps/desktop/src/sidebar/settings.tsx | 2 + .../src/store/tinybase/store/settings.ts | 30 ++ apps/desktop/src/store/zustand/tabs/schema.ts | 2 + apps/web/src/routes/_view/pricing.tsx | 2 +- .../template-app/assets/chat.system.md.jinja | 39 +++ crates/template-app/src/chat.rs | 32 ++ packages/pricing/src/tiers.ts | 8 +- packages/store/src/tinybase.ts | 6 + packages/store/src/zod.ts | 6 + plugins/template/js/bindings.gen.ts | 2 +- 16 files changed, 482 insertions(+), 47 deletions(-) create mode 100644 apps/desktop/src/settings/personalization/index.tsx diff --git a/apps/desktop/src/ai/prompts/details.tsx b/apps/desktop/src/ai/prompts/details.tsx index 16272d5989..3dccbe8e14 100644 --- a/apps/desktop/src/ai/prompts/details.tsx +++ b/apps/desktop/src/ai/prompts/details.tsx @@ -23,7 +23,7 @@ export function PromptDetailsColumn({ return (

- Select a task type to view or customize its prompt + Select a meeting-note task to view or customize its instructions

); @@ -163,7 +163,7 @@ function PromptDetails({ selectedTask }: { selectedTask: TaskType }) { diff --git a/apps/desktop/src/ai/prompts/index.tsx b/apps/desktop/src/ai/prompts/index.tsx index ad15be2ba5..6c48b8a308 100644 --- a/apps/desktop/src/ai/prompts/index.tsx +++ b/apps/desktop/src/ai/prompts/index.tsx @@ -28,7 +28,7 @@ export const TabItemPrompt: TabItem> = ({ return ( } - title={"Prompts"} + title={"Meeting Notes"} selected={tab.active} pinned={tab.pinned} tabIndex={tabIndex} diff --git a/apps/desktop/src/ai/prompts/list.tsx b/apps/desktop/src/ai/prompts/list.tsx index e401f820b2..98acb656ae 100644 --- a/apps/desktop/src/ai/prompts/list.tsx +++ b/apps/desktop/src/ai/prompts/list.tsx @@ -15,7 +15,7 @@ export function PromptsListColumn({ return (
-

Custom Prompts

+

Meeting Notes

diff --git a/apps/desktop/src/chat/transport/use-transport.ts b/apps/desktop/src/chat/transport/use-transport.ts index 47a1d7a05b..85eeac9f19 100644 --- a/apps/desktop/src/chat/transport/use-transport.ts +++ b/apps/desktop/src/chat/transport/use-transport.ts @@ -1,5 +1,6 @@ +import { useQuery } from "@tanstack/react-query"; import type { LanguageModel, ToolSet } from "ai"; -import { useEffect, useMemo, useState } from "react"; +import { useMemo } from "react"; import { commands as templateCommands } from "@hypr/plugin-template"; @@ -10,6 +11,7 @@ import { useLanguageModel } from "~/ai/hooks"; import type { ContextRef } from "~/chat/context/entities"; import { hydrateSessionContextFromFs } from "~/chat/context/session-context-hydrator"; import { useToolRegistry } from "~/contexts/tool"; +import { useConfigValues } from "~/shared/config"; import * as main from "~/store/tinybase/store/main"; function renderHumanContext( @@ -82,49 +84,59 @@ export function useTransport( const registry = useToolRegistry(); const configuredModel = useLanguageModel("chat"); const model = modelOverride ?? configuredModel; - const language = main.UI.useValue("ai_language", main.STORE_ID) ?? "en"; - const [systemPrompt, setSystemPrompt] = useState(); - - useEffect(() => { - if (systemPromptOverride) { - setSystemPrompt(systemPromptOverride); - return; - } - - let stale = false; - - templateCommands - .render({ + const { + ai_language: language, + chat_style_tone: styleTone, + chat_warmth: warmth, + chat_enthusiasm: enthusiasm, + chat_headers_lists: headersLists, + chat_emoji: emoji, + chat_custom_instructions: customInstructions, + } = useConfigValues([ + "ai_language", + "chat_style_tone", + "chat_warmth", + "chat_enthusiasm", + "chat_headers_lists", + "chat_emoji", + "chat_custom_instructions", + ] as const); + const normalizedCustomInstructions = customInstructions.trim(); + + const systemPromptQuery = useQuery({ + queryKey: [ + "chat-system-prompt", + language, + styleTone, + warmth, + enthusiasm, + headersLists, + emoji, + normalizedCustomInstructions, + ], + enabled: systemPromptOverride === undefined, + staleTime: Infinity, + queryFn: async () => { + const result = await templateCommands.render({ chatSystem: { language, + styleTone, + warmth, + enthusiasm, + headersLists, + emoji, + customInstructions: normalizedCustomInstructions, }, - }) - .then((result) => { - if (stale) { - return; - } - - if (result.status === "ok") { - setSystemPrompt(result.data); - } else { - setSystemPrompt(""); - } - }) - .catch((error) => { - console.error(error); - if (!stale) { - setSystemPrompt(""); - } }); - return () => { - stale = true; - }; - }, [language, systemPromptOverride]); + return result.status === "ok" ? result.data : ""; + }, + }); - const effectiveSystemPrompt = systemPromptOverride ?? systemPrompt; + const effectiveSystemPrompt = systemPromptOverride ?? systemPromptQuery.data; const isSystemPromptReady = - typeof systemPromptOverride === "string" || systemPrompt !== undefined; + typeof systemPromptOverride === "string" || + systemPromptQuery.data !== undefined; const tools = useMemo(() => { const localTools = registry.getTools("chat-general"); diff --git a/apps/desktop/src/settings/index.tsx b/apps/desktop/src/settings/index.tsx index 7114247334..5c030f0121 100644 --- a/apps/desktop/src/settings/index.tsx +++ b/apps/desktop/src/settings/index.tsx @@ -12,6 +12,7 @@ import { import { SettingsLab } from "./lab"; import { AgentIntegrations } from "./lab/agent-integrations"; import { DeveloperSettings } from "./lab/developer"; +import { SettingsPersonalization } from "./personalization"; import { SettingsTodo } from "./todo"; import { LLM } from "~/settings/ai/llm"; @@ -85,6 +86,8 @@ function SettingsView({ tab }: { tab: Extract }) { return ; case "intelligence": return ; + case "personalization": + return ; case "memory": return ; case "todo": diff --git a/apps/desktop/src/settings/personalization/index.tsx b/apps/desktop/src/settings/personalization/index.tsx new file mode 100644 index 0000000000..12ab1d9dd1 --- /dev/null +++ b/apps/desktop/src/settings/personalization/index.tsx @@ -0,0 +1,303 @@ +import { useForm } from "@tanstack/react-form"; + +import { Button } from "@hypr/ui/components/ui/button"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@hypr/ui/components/ui/select"; +import { Textarea } from "@hypr/ui/components/ui/textarea"; + +import { SettingsPageTitle } from "~/settings/page-title"; +import { useConfigValues } from "~/shared/config"; +import * as settings from "~/store/tinybase/store/settings"; +import { type Tab, useTabs } from "~/store/zustand/tabs"; + +const PERSONALIZATION_KEYS = [ + "chat_style_tone", + "chat_warmth", + "chat_enthusiasm", + "chat_headers_lists", + "chat_emoji", + "chat_custom_instructions", +] as const; + +const STYLE_TONE_OPTIONS = [ + { value: "professional", label: "Professional" }, + { value: "friendly", label: "Friendly" }, + { value: "concise", label: "Concise" }, + { value: "technical", label: "Technical" }, +] as const; + +const CHARACTERISTIC_OPTIONS = [ + { value: "default", label: "Default" }, + { value: "more", label: "More" }, + { value: "less", label: "Less" }, +] as const; + +const EMOJI_OPTIONS = [ + { value: "default", label: "Default" }, + { value: "less", label: "Less" }, + { value: "none", label: "None" }, +] as const; + +function usePersonalizationForm() { + const values = useConfigValues(PERSONALIZATION_KEYS); + + const setPartialValues = settings.UI.useSetPartialValuesCallback( + (row: Partial>) => + row, + [], + settings.STORE_ID, + ); + + return useForm({ + defaultValues: { + chat_style_tone: values.chat_style_tone, + chat_warmth: values.chat_warmth, + chat_enthusiasm: values.chat_enthusiasm, + chat_headers_lists: values.chat_headers_lists, + chat_emoji: values.chat_emoji, + chat_custom_instructions: values.chat_custom_instructions, + }, + listeners: { + onChange: ({ formApi }) => { + void formApi.handleSubmit(); + }, + }, + onSubmit: ({ value }) => { + setPartialValues(value); + }, + }); +} + +export function SettingsPersonalization() { + const form = usePersonalizationForm(); + const tabs = useTabs((state) => state.tabs); + const openNew = useTabs((state) => state.openNew); + const select = useTabs((state) => state.select); + const updatePromptsTabState = useTabs((state) => state.updatePromptsTabState); + + const openMeetingNotesEditor = () => { + const promptsTab = tabs.find( + (tab): tab is Extract => tab.type === "prompts", + ); + + if (!promptsTab) { + openNew({ + type: "prompts", + state: { + selectedTask: "enhance", + }, + }); + return; + } + + updatePromptsTabState(promptsTab, { + ...promptsTab.state, + selectedTask: "enhance", + }); + select(promptsTab); + }; + + return ( +
+ + +
+
+

Chat

+

+ Adjust Charlie's default voice, formatting, and response rules. +

+
+ +
+ + {(field) => ( + + )} + + + + {(field) => ( + + )} + + + + {(field) => ( + + )} + + + + {(field) => ( + + )} + + + + {(field) => ( + + )} + + + + {(field) => ( + + )} + +
+
+ +
+
+

+ Meeting Notes +

+

+ Open the advanced editor for meeting-note and title-generation + prompts. +

+
+ + +
+
+ ); +} + +function SelectSettingRow({ + title, + description, + value, + onChange, + options, +}: { + title: string; + description: string; + value: string; + onChange: (value: string) => void; + options: ReadonlyArray<{ value: string; label: string }>; +}) { + return ( +
+
+

{title}

+

{description}

+
+ + +
+ ); +} + +function TextareaSettingRow({ + title, + description, + value, + onChange, + placeholder, +}: { + title: string; + description: string; + value: string; + onChange: (value: string) => void; + placeholder: string; +}) { + return ( +
+
+

{title}

+

{description}

+
+ +