Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 39 additions & 2 deletions apps/desktop/src/components/editor-area/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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[^>]*>(.*?)<\/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 <h1> tags)

const finalInput = diffWords(preMeetingText, rawText)
?.filter(diff => diff.added && !diff.removed)
.map(diff => diff.value)
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -372,7 +409,7 @@ export function useEnhanceMutation({
metadata: {
grammar: {
task: "enhance",
sections: selectedTemplate?.sections.map(s => s.title) || null,
sections: grammarSections,
} satisfies Grammar,
},
},
Expand Down
122 changes: 122 additions & 0 deletions apps/desktop/src/components/settings/views/ai.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -121,6 +123,30 @@ const initialLlmModels = [
},
];

const aiConfigSchema = z.object({
aiSpecificity: z.number().int().min(1).max(4).optional(),
});
type AIConfigValues = z.infer<typeof aiConfigSchema>;

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);
Expand Down Expand Up @@ -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<AIConfigValues>({
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<FormValues>({
resolver: zodResolver(endpointSchema),
mode: "onChange",
Expand Down Expand Up @@ -682,6 +749,61 @@ export default function LocalAI() {
</FormItem>
)}
/>

{/* NEW: Detail Level Configuration */}
<Form {...aiConfigForm}>
<FormField
control={aiConfigForm.control}
name="aiSpecificity"
render={({ field }) => (
<FormItem>
<FormLabel className="text-sm font-medium">
<Trans>Creativity Level</Trans>
</FormLabel>
<FormDescription className="text-xs">
<Trans>Control how creative the AI enhancement should be</Trans>
</FormDescription>
<FormControl>
<div className="space-y-3">
{/* Button bar - matching form element width */}
<div className="w-full">
<div className="flex justify-between rounded-md p-0.5 bg-gradient-to-r from-blue-500 via-indigo-500 to-purple-500 shadow-sm">
{[1, 2, 3, 4].map((level) => (
<button
key={level}
type="button"
onClick={() => {
field.onChange(level);
aiConfigMutation.mutate({ aiSpecificity: level });
}}
disabled={!customLLMEnabled.data}
className={cn(
"py-1.5 px-2 flex-1 text-center text-sm font-medium rounded transition-all duration-150 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-offset-transparent",
field.value === level
? "bg-white text-black shadow-sm"
: "text-white hover:bg-white/20",
!customLLMEnabled.data && "opacity-50 cursor-not-allowed"
)}
>
{specificityLevels[level as keyof typeof specificityLevels]?.title}
</button>
))}
</div>
</div>

{/* Current selection description in card */}
<div className="p-3 rounded-md bg-neutral-50 border border-neutral-200">
<div className="text-xs text-muted-foreground">
{specificityLevels[field.value as keyof typeof specificityLevels]?.description || specificityLevels[3].description}
</div>
</div>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</Form>
</form>
</Form>
</div>
Expand Down
12 changes: 11 additions & 1 deletion crates/db-user/src/config_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,20 @@ impl Default for ConfigNotification {
}

user_common_derives! {
#[derive(Default)]
pub struct ConfigAI {
pub api_base: Option<String>,
pub api_key: Option<String>,
pub ai_specificity: Option<u8>,
}
}

impl Default for ConfigAI {
fn default() -> Self {
Self {
api_base: None,
api_key: None,
ai_specificity: Some(3),
}
}
}

Expand Down
59 changes: 57 additions & 2 deletions crates/template/assets/enhance.system.jinja
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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(<u>text</u>), 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 }}.
Expand All @@ -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

Expand Down Expand Up @@ -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 %}
3 changes: 3 additions & 0 deletions crates/template/assets/enhance.user.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -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 <thinking></thinking> tags, and then write the note based on the headers.

Expand Down
1 change: 1 addition & 0 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading
Loading