From 9535eedf091c0ee7938e77a4bd983dda2f07e0d3 Mon Sep 17 00:00:00 2001 From: Deokhaeng Lee Date: Fri, 18 Jul 2025 17:59:58 -0700 Subject: [PATCH] added detail configurations --- .../src/components/editor-area/index.tsx | 52 +++++++- .../src/components/settings/views/ai.tsx | 123 ++++++++++++++++++ crates/db-user/src/config_types.rs | 12 +- crates/template/assets/enhance.system.jinja | 59 ++++++++- crates/template/assets/enhance.user.jinja | 3 + packages/ui/package.json | 1 + packages/ui/src/components/ui/slider.tsx | 81 ++++++++++++ pnpm-lock.yaml | 35 +++++ 8 files changed, 361 insertions(+), 5 deletions(-) create mode 100644 packages/ui/src/components/ui/slider.tsx diff --git a/apps/desktop/src/components/editor-area/index.tsx b/apps/desktop/src/components/editor-area/index.tsx index 811442373..e0bf8e3a1 100644 --- a/apps/desktop/src/components/editor-area/index.tsx +++ b/apps/desktop/src/components/editor-area/index.tsx @@ -258,9 +258,34 @@ export function useEnhanceMutation({ const [actualIsLocalLlm, setActualIsLocalLlm] = useState(isLocalLlm); const queryClient = useQueryClient(); + // ✅ Extract H1 headers at component level (always available) + const extractH1Headers = useCallback((htmlContent: string): string[] => { + if (!htmlContent) return []; + + const h1Regex = /]*>(.*?)<\/h1>/gi; + const headers: string[] = []; + let match; + + while ((match = h1Regex.exec(htmlContent)) !== null) { + const headerText = match[1].replace(/<[^>]*>/g, '').trim(); + if (headerText) { + headers.push(headerText); + } + } + + return headers; + }, []); + + // ✅ Memoized h1Headers (recalculates when rawContent changes) + const h1Headers = useMemo(() => extractH1Headers(rawContent), [rawContent, extractH1Headers]); + const preMeetingText = extractTextFromHtml(preMeetingNote); const rawText = extractTextFromHtml(rawContent); + console.log("Extracted H1 headers:", h1Headers); + + //i only want to extract headers from the rawContent (the ones that are cased in

tags) + const finalInput = diffWords(preMeetingText, rawText) ?.filter(diff => diff.added && !diff.removed) .map(diff => diff.value) @@ -315,11 +340,34 @@ export function useEnhanceMutation({ const selectedTemplate = await TemplateService.getTemplate(effectiveTemplateId ?? ""); + // ✅ SMART GRAMMAR: Use H1 headers when auto/default AND headers exist + const shouldUseH1Headers = !effectiveTemplateId && h1Headers.length > 0; + const grammarSections = shouldUseH1Headers + ? h1Headers + : selectedTemplate?.sections.map(s => s.title) || null; + + /* + console.log("Enhancement strategy:", { + effectiveTemplateId, + h1HeadersCount: h1Headers.length, + shouldUseH1Headers, + grammarSections + }); + */ + const participants = await dbCommands.sessionListParticipants(sessionId); const systemMessage = await templateCommands.render( "enhance.system", - { config, type, templateInfo: selectedTemplate }, + { + config, + type, + // ✅ Pass userHeaders when using H1 headers, templateInfo otherwise + ...(shouldUseH1Headers + ? { userHeaders: h1Headers } // ✅ Pass h1Headers array + : { templateInfo: selectedTemplate } // ✅ Pass template only when not using headers + ) + }, ); const userMessage = await templateCommands.render( @@ -372,7 +420,7 @@ export function useEnhanceMutation({ metadata: { grammar: { task: "enhance", - sections: selectedTemplate?.sections.map(s => s.title) || null, + sections: grammarSections, // ✅ Dynamic sections } satisfies Grammar, }, }, diff --git a/apps/desktop/src/components/settings/views/ai.tsx b/apps/desktop/src/components/settings/views/ai.tsx index b78c2c3bc..ec621a413 100644 --- a/apps/desktop/src/components/settings/views/ai.tsx +++ b/apps/desktop/src/components/settings/views/ai.tsx @@ -9,6 +9,7 @@ import { showLlmModelDownloadToast, showSttModelDownloadToast } from "../../toas import { commands as connectorCommands, type Connection } from "@hypr/plugin-connector"; import { commands as localLlmCommands, SupportedModel } from "@hypr/plugin-local-llm"; +import { commands as dbCommands } from "@hypr/plugin-db"; import { Button } from "@hypr/ui/components/ui/button"; import { @@ -26,6 +27,8 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { Tooltip, TooltipContent, TooltipTrigger } from "@hypr/ui/components/ui/tooltip"; import { cn } from "@hypr/ui/lib/utils"; import { WERPerformanceModal } from "../components/wer-modal"; +import { Slider } from "@hypr/ui/components/ui/slider"; +import { Card, CardContent, CardHeader, CardTitle } from "@hypr/ui/components/ui/card"; const endpointSchema = z.object({ model: z.string().min(1), @@ -120,6 +123,30 @@ const initialLlmModels = [ }, ]; +const aiConfigSchema = z.object({ + aiSpecificity: z.number().int().min(1).max(4).optional(), +}); +type AIConfigValues = z.infer; + +const specificityLevels = { + 1: { + title: "Minimal", + description: "High-level summaries only. Focuses on key points and main takeaways without detailed explanations." + }, + 2: { + title: "Moderate", + description: "Balanced detail level. Includes important context and supporting information while staying concise." + }, + 3: { + title: "Detailed", + description: "Include specifics and examples. Provides comprehensive coverage with relevant details and explanations." + }, + 4: { + title: "Comprehensive", + description: "Maximum detail level. Captures all nuances, background context, and thorough analysis of the content." + } +} as const; + export default function LocalAI() { const queryClient = useQueryClient(); const [isWerModalOpen, setIsWerModalOpen] = useState(false); @@ -240,6 +267,47 @@ export default function LocalAI() { }, }); + const config = useQuery({ + queryKey: ["config", "ai"], + queryFn: async () => { + const result = await dbCommands.getConfig(); + return result; + }, + }); + + const aiConfigForm = useForm({ + resolver: zodResolver(aiConfigSchema), + defaultValues: { + aiSpecificity: 3, + }, + }); + + useEffect(() => { + if (config.data) { + aiConfigForm.reset({ + aiSpecificity: config.data.ai.ai_specificity ?? 3, + }); + } + }, [config.data, aiConfigForm]); + + const aiConfigMutation = useMutation({ + mutationFn: async (values: AIConfigValues) => { + if (!config.data) return; + + await dbCommands.setConfig({ + ...config.data, + ai: { + ...config.data.ai, + ai_specificity: values.aiSpecificity, + }, + }); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["config", "ai"] }); + }, + onError: console.error, + }); + const form = useForm({ resolver: zodResolver(endpointSchema), mode: "onChange", @@ -680,6 +748,61 @@ export default function LocalAI() { )} /> + + {/* NEW: Detail Level Configuration */} +
+ ( + + + Detail Level + + + Control how detailed the AI enhancement should be + + +
+ {/* Button bar - matching form element width */} +
+
+ {[1, 2, 3, 4].map((level) => ( + + ))} +
+
+ + {/* Current selection description in card */} +
+
+ {specificityLevels[field.value as keyof typeof specificityLevels]?.description || specificityLevels[3].description} +
+
+
+
+ +
+ )} + /> + diff --git a/crates/db-user/src/config_types.rs b/crates/db-user/src/config_types.rs index 9940d13f4..b8b85977e 100644 --- a/crates/db-user/src/config_types.rs +++ b/crates/db-user/src/config_types.rs @@ -81,10 +81,20 @@ impl Default for ConfigNotification { } user_common_derives! { - #[derive(Default)] pub struct ConfigAI { pub api_base: Option, pub api_key: Option, + pub ai_specificity: Option, + } +} + +impl Default for ConfigAI { + fn default() -> Self { + Self { + api_base: None, + api_key: None, + ai_specificity: Some(3), + } } } diff --git a/crates/template/assets/enhance.system.jinja b/crates/template/assets/enhance.system.jinja index 0b7bf1a54..2f88766d4 100644 --- a/crates/template/assets/enhance.system.jinja +++ b/crates/template/assets/enhance.system.jinja @@ -1,6 +1,33 @@ You are a professional assistant that generates enhanced meetings notes while maintaining accuracy, completeness, and professional terminology in {{ config.general.display_language | language }}. -{% if templateInfo %} +{%- set specificity = config.ai.ai_specificity | default(3) %} + +{% if userHeaders %} +The user has structured their content with these headers: +{% for header in userHeaders %}"{{ header }}"{% if not loop.last %}, {% endif %}{% endfor %} + + {%- if specificity == 1 %} + + **Adherence Level: Strict** + You must strictly follow these exact headers. Use them as-is without modifications. Focus on concise, high-level content under each header. + + {% elif specificity == 2 %} + + **Adherence Level: Mostly Strict** + You must mainly follow these headers, but are allowed to make minor changes or enhancements if necessary for clarity or professionalism. + + {% elif specificity == 3 %} + + **Adherence Level: Flexible** + You should follow the general structure of these headers, but feel free to rename them for better clarity, professionalism, or accuracy. + + {% elif specificity == 4 %} + + **Adherence Level: Creative** + Use these headers as inspiration for your enhanced notes. Generate new headers based on these, but make changes if you think you can improve them. Add additional sections if the content warrants it. + {% endif %} + +{% elif templateInfo %} The user has provided a custom template that defines the structure and sections for the enhanced meeting notes. Your response must strictly follow this template's format and section headers. @@ -54,6 +81,31 @@ You will be given multiple inputs from the user. Below are useful information th - Preserve essential details; avoid excessive abstraction. Ensure content remains concrete and specific. - Pay close attention to emphasized text in raw notes. Users highlight information using four styles: bold(**text**), italic(_text_), underline(text), strikethrough(~~text~~). - Recognize H3 headers (### Header) in raw notes—these indicate highly important topics that the user wants to retain no matter what. +- Below is the guideline on how many changes you should make to the original raw note, please pay close atteion: + + {%- if specificity == 1 %} + + **Creativity Level: low** + User already knows and has a specific taste/stance about the raw note. Make only minimal changes to the raw notes if necessary. Focus on fixing typos, improving readability, and organizing content while preserving the original structure and meaning. + Only add new contents to the raw note if user didn't write anything to section headers. + + {% elif specificity == 2 %} + + **Creativity Level: moderate** + User has overall idea about how the note should look like. Enhance readability and organization while maintaining core content. Add relevant details from the transcript to provide more context and clarity, but preserve the main structure and key points from raw notes. + + {% elif specificity == 3 %} + + **Creativity Level: high** + Significantly enhance the raw notes by incorporating relevant information from the transcript. Reorganize content into logical sections, expand on key points, and add important context while ensuring the original intent is preserved. Focus on creating a comprehensive and well-structured document. + + {% elif specificity == 4 %} + + **Creativity Level: very high** + Create a thorough and polished document by fully integrating raw notes with transcript content. Reorganize extensively, add detailed context and explanations, and create clear thematic sections. Focus on producing a professional-quality document while preserving key insights from the raw notes. + + {% endif %} + {% if config.general.display_language is not english %} - Keep technical terms (e.g., API, SDK, frontend, backend) and globally recognized product names (e.g., React, Vue.js, Django) in English. - When using technical terms in sentences, follow the grammatical rules of {{ config.general.display_language | language }}. @@ -62,7 +114,9 @@ You will be given multiple inputs from the user. Below are useful information th - 문장 끝을 **"-했습니다" 대신 "-했음"**처럼 간결하게 줄임. {% endif %} -# Correct Examples of a Section + +{% if specificity == 3 or specificity == 4 %} +# Correct Examples of a Section ## Example 1 @@ -110,5 +164,6 @@ You will be given multiple inputs from the user. Below are useful information th - Need high-ticket items for sustainability - Importance of finding strong strategic partners ``` +{% endif %} {% endif %} diff --git a/crates/template/assets/enhance.user.jinja b/crates/template/assets/enhance.user.jinja index 67b80c3b7..17694ac80 100644 --- a/crates/template/assets/enhance.user.jinja +++ b/crates/template/assets/enhance.user.jinja @@ -15,6 +15,9 @@ Your job is to write a perfect note based on the above informations. Note that above given informations like participants, transcript, etc. are already displayed in the UI, so you don't need to repeat them. +MAKE SURE THAT contents in the 'raw_note' is well incorporated in the final enhanced note. It is paramount that the enhanced note contains contents +of the raw note. + {% if type == "HyprLocal" %} Also, before writing enhanced note, write multiple top-level headers inside tags, and then write the note based on the headers. diff --git a/packages/ui/package.json b/packages/ui/package.json index ec18ec333..f994f02d9 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -30,6 +30,7 @@ "@radix-ui/react-scroll-area": "^1.2.9", "@radix-ui/react-select": "^2.2.5", "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slider": "^1.3.5", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.2.5", "@radix-ui/react-tabs": "^1.1.12", diff --git a/packages/ui/src/components/ui/slider.tsx b/packages/ui/src/components/ui/slider.tsx new file mode 100644 index 000000000..744b39bbc --- /dev/null +++ b/packages/ui/src/components/ui/slider.tsx @@ -0,0 +1,81 @@ +import * as SliderPrimitive from "@radix-ui/react-slider"; +import { cva, type VariantProps } from "class-variance-authority"; +import * as React from "react"; + +import { cn } from "../../lib/utils"; + +const sliderVariants = cva( + "relative flex w-full touch-none select-none items-center", + { + variants: { + size: { + sm: "h-4", + default: "h-5", + lg: "h-6", + }, + }, + defaultVariants: { + size: "default", + }, + }, +); + +const trackVariants = cva( + "relative h-2 w-full grow overflow-hidden rounded-full bg-input", + { + variants: { + size: { + sm: "h-1.5", + default: "h-2", + lg: "h-2.5", + }, + }, + defaultVariants: { + size: "default", + }, + }, +); + +const rangeVariants = cva( + "absolute h-full bg-primary", + {} +); + +const thumbVariants = cva( + "block rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + { + variants: { + size: { + sm: "h-4 w-4", + default: "h-5 w-5", + lg: "h-6 w-6", + }, + }, + defaultVariants: { + size: "default", + }, + }, +); + +export interface SliderProps + extends React.ComponentPropsWithoutRef, + VariantProps {} + +const Slider = React.forwardRef< + React.ElementRef, + SliderProps +>(({ className, size, ...props }, ref) => ( + + + + + + +)); +Slider.displayName = SliderPrimitive.Root.displayName; + +export { Slider }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2ed87724e..25ad2367b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -776,6 +776,9 @@ importers: '@radix-ui/react-separator': specifier: ^1.1.7 version: 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slider': + specifier: ^1.3.5 + version: 1.3.5(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': specifier: ^1.2.3 version: 1.2.3(@types/react@18.3.23)(react@18.3.1) @@ -2999,6 +3002,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-slider@1.3.5': + resolution: {integrity: sha512-rkfe2pU2NBAYfGaxa3Mqosi7VZEWX5CxKaanRv0vZd4Zhl9fvQrg0VM93dv3xGLGfrHuoTRF3JXH8nb9g+B3fw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-slot@1.2.3': resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} peerDependencies: @@ -12024,6 +12040,25 @@ snapshots: '@types/react': 18.3.23 '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-slider@1.3.5(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-slot@1.2.3(@types/react@18.3.23)(react@18.3.1)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1)