diff --git a/apps/desktop/src/components/editor-area/index.tsx b/apps/desktop/src/components/editor-area/index.tsx index 811442373..27a086edb 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,23 @@ export function useEnhanceMutation({ const selectedTemplate = await TemplateService.getTemplate(effectiveTemplateId ?? ""); + const shouldUseH1Headers = !effectiveTemplateId && h1Headers.length > 0; + const grammarSections = selectedTemplate?.sections.map(s => s.title) || null; + + 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 +409,7 @@ export function useEnhanceMutation({ metadata: { grammar: { task: "enhance", - sections: selectedTemplate?.sections.map(s => s.title) || null, + sections: grammarSections, } satisfies Grammar, }, }, diff --git a/apps/desktop/src/components/settings/views/ai.tsx b/apps/desktop/src/components/settings/views/ai.tsx index 3585d07df..0dc03f0f9 100644 --- a/apps/desktop/src/components/settings/views/ai.tsx +++ b/apps/desktop/src/components/settings/views/ai.tsx @@ -9,6 +9,8 @@ import { z } from "zod"; 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 { commands as localSttCommands } from "@hypr/plugin-local-stt"; import { Button } from "@hypr/ui/components/ui/button"; import { @@ -121,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: "Conservative", + description: "Minimal creative changes. Preserves your original writing style and content while making only essential improvements to clarity and flow." + }, + 2: { + title: "Balanced", + description: "Moderate creative input. Enhances your content with some stylistic improvements while maintaining the core message and tone." + }, + 3: { + title: "Creative", + description: "More creative freedom. Actively improves and expands content with additional context, examples, and engaging language." + }, + 4: { + title: "Innovative", + description: "Maximum creativity. Transforms content with rich language, fresh perspectives, and creative restructuring while preserving key information." + } +} as const; + export default function LocalAI() { const queryClient = useQueryClient(); const [isWerModalOpen, setIsWerModalOpen] = useState(false); @@ -242,6 +268,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", @@ -682,6 +749,61 @@ export default function LocalAI() { )} /> + + {/* NEW: Detail Level Configuration */} +
+ ( + + + Creativity Level + + + Control how creative 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 e97b2e7a9..ec0a2a660 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: @@ -12028,6 +12044,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)