Skip to content
Merged
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
44 changes: 40 additions & 4 deletions apps/desktop/src/components/editor-area/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { type QueryClient, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import usePreviousValue from "beautiful-react-hooks/usePreviousValue";
import { diffWords } from "diff";
import { motion } from "motion/react";
Expand All @@ -8,6 +8,7 @@ import { z } from "zod";

import { useHypr } from "@/contexts";
import { extractTextFromHtml } from "@/utils/parse";
import { autoTagGeneration } from "@/utils/tag-generation";
import { TemplateService } from "@/utils/template-service";
import { commands as analyticsCommands } from "@hypr/plugin-analytics";
import { commands as connectorCommands } from "@hypr/plugin-connector";
Expand All @@ -27,7 +28,12 @@ import { FloatingButton } from "./floating-button";
import { NoteHeader } from "./note-header";
import { TextSelectionPopover } from "./text-selection-popover";

async function generateTitleDirect(enhancedContent: string, targetSessionId: string, sessions: Record<string, any>) {
async function generateTitleDirect(
enhancedContent: string,
targetSessionId: string,
sessions: Record<string, any>,
queryClient: QueryClient,
) {
const [config, { type }, provider] = await Promise.all([
dbCommands.getConfig(),
connectorCommands.getLlmConnection(),
Expand Down Expand Up @@ -58,6 +64,36 @@ async function generateTitleDirect(enhancedContent: string, targetSessionId: str
if (!session?.title && sessions[targetSessionId]?.getState) {
const cleanedTitle = text.replace(/^["']|["']$/g, "").trim();
sessions[targetSessionId].getState().updateTitle(cleanedTitle);

try {
const suggestedTags = await autoTagGeneration(targetSessionId);

if (suggestedTags.length > 1) {
const allExistingTags = await dbCommands.listAllTags();
const existingTagsMap = new Map(
allExistingTags.map(tag => [tag.name.toLowerCase(), tag]),
);

for (const tagName of suggestedTags.slice(0, 2)) {
try {
const existingTag = existingTagsMap.get(tagName.toLowerCase());

const tag = await dbCommands.upsertTag({
id: existingTag?.id || crypto.randomUUID(),
name: tagName,
});

await dbCommands.assignTagToSession(tag.id, targetSessionId);
} catch (error) {
console.error(`Failed to assign tag "${tagName}":`, error);
}
}

queryClient.invalidateQueries({ queryKey: ["session-tags", targetSessionId] });
}
} catch (error) {
console.error("Failed to generate tags:", error);
}
}
}

Expand Down Expand Up @@ -108,15 +144,15 @@ export default function EditorArea({
});

const sessionsStore = useSessions((s) => s.sessions);

const queryClient = useQueryClient();
const { enhance, progress, isCancelled } = useEnhanceMutation({
sessionId,
preMeetingNote,
rawContent,
isLocalLlm: llmConnectionQuery.data?.type === "HyprLocal",
onSuccess: (content) => {
if (hasTranscriptWords) {
generateTitleDirect(content, sessionId, sessionsStore).catch(console.error);
generateTitleDirect(content, sessionId, sessionsStore, queryClient).catch(console.error);
}
},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,11 @@ export function TagChip({ sessionId, hashtags = [], isVeryNarrow = false, isNarr
return isVeryNarrow ? "Tags" : isNarrow ? "Tags" : "Add tags";
}

if (isVeryNarrow) {
// show just the number when narrow (matching participants-chip)
if (isVeryNarrow || isNarrow) {
return totalTags.toString();
}

if (isNarrow && firstTag && firstTag.length > 8) {
const truncated = firstTag.slice(0, 6) + "...";
return additionalTags > 0 ? `${truncated} +${additionalTags}` : truncated;
}

return additionalTags > 0 ? `${firstTag} +${additionalTags}` : firstTag;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,11 @@ export function TranscriptView() {
/>
)
: (
<header className="flex items-center justify-between w-full px-4 py-1 my-1 border-b border-neutral-100">
<header
className={`flex items-center justify-between w-full px-4 py-1 my-1 ${
!showEmptyMessage ? "border-b border-neutral-100" : ""
}`}
>
{!showEmptyMessage && (
<div className="flex items-center gap-2">
<h2 className="text-sm font-semibold text-neutral-900">Transcript</h2>
Expand Down
75 changes: 74 additions & 1 deletion apps/desktop/src/utils/tag-generation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,84 @@ export async function generateTagsForSession(sessionId: string): Promise<string[
{ config, type: connectionType },
);

let contentToUse = session.enhanced_memo_html ?? session.raw_memo_html;

const userPrompt = await templateCommands.render(
"suggest_tags.user",
{
title: session.title,
content: session.raw_memo_html,
content: contentToUse,
existing_hashtags: existingHashtags,
formal_tags: currentTags.map(t => t.name),
historical_tags: historicalTags.slice(0, 20).map(t => t.name),
},
);

const provider = await modelProvider();
const model = provider.languageModel("defaultModel");

const result = await generateText({
model,
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: userPrompt },
],
providerOptions: {
[localProviderName]: {
metadata: {
grammar: {
task: "tags",
} satisfies Grammar,
},
},
},
});

const schema = z.preprocess(
(val) => (typeof val === "string" ? JSON.parse(val) : val),
z.array(z.string().min(1)).min(1).max(5),
);

const parsed = schema.safeParse(result.text);
return parsed.success ? parsed.data : [];
} catch (error) {
console.error("Tag generation failed:", error);
return [];
}
}

export async function autoTagGeneration(sessionId: string): Promise<string[]> {
try {
const { type: connectionType } = await connectorCommands.getLlmConnection();

const config = await dbCommands.getConfig();
const session = await dbCommands.getSession({ id: sessionId });
if (!session) {
throw new Error("Session not found");
}

const historicalTags = await dbCommands.listAllTags();
const currentTags = await dbCommands.listSessionTags(sessionId);

const extractHashtags = (text: string): string[] => {
const hashtagRegex = /#(\w+)/g;
return Array.from(text.matchAll(hashtagRegex), match => match[1]);
};

const existingHashtags = extractHashtags(session.raw_memo_html);

const systemPrompt = await templateCommands.render(
"auto_generate_tags.system",
{ config, type: connectionType },
);

let contentToUse = session.enhanced_memo_html ?? session.raw_memo_html;

const userPrompt = await templateCommands.render(
"auto_generate_tags.user",
{
title: session.title,
content: contentToUse,
existing_hashtags: existingHashtags,
formal_tags: currentTags.map(t => t.name),
historical_tags: historicalTags.slice(0, 20).map(t => t.name),
Expand Down
23 changes: 23 additions & 0 deletions crates/template/assets/auto_generate_tags.system.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
You are an intelligent tagging assistant.
Your job is to analyze note content and existing tags to suggest relevant, useful tags based on the content and the user's historical tagging patterns.

## Guidelines:

1. Suggest minimum 3, maximum 5 specific, actionable tags
2. Always look first at existing tags (which will be named 'historical_tags' in the user prompt) and if there are any that are relevant to the content, use them. You must return the exact same tags - don't change them if you are reusing them.
3. Also, don't return tags that are already in the content.
- Two types of tags that are not relevant to the content:
- Hashtags (which will be named 'existing_hashtags' in the user prompt)
- Formal tags (which will be named 'formal_tags' in the user prompt)
4. Only suggest new tags if you can't find any relevant existing tags.
- Before coming up with your creative ideas, consider choosing from these default options:
["Sales", "User Interview", "Product", "Marketing", "Engineering", "Customer Support", "Research", "Insight", "Design", "Recruiting"]
5. You can return a combination of existing tags and new tags.
6. Avoid generic tags like "note" or "content"
7. Make tags specific enough to be useful for search and organization
8. Consider meeting types, project names, people, tools, concepts, and workflows

## Response Format:

Return only a JSON array of suggested tag names, nothing else.
Example: ["project-alpha", "team-meeting", "quarterly-planning", "action-items"]
29 changes: 29 additions & 0 deletions crates/template/assets/auto_generate_tags.user.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
## Note Content:

**Title:** {{ title }}

**Content:**
{{ content }}

{% if existing_hashtags and existing_hashtags|length > 0 %}

## Current Hashtags in Content:

{% for hashtag in existing_hashtags %}#{{ hashtag }}{% if not loop.last %}, {% endif %}{% endfor %}
{% endif %}

{% if formal_tags and formal_tags|length > 0 %}

## Current Formal Tags:

{% for tag in formal_tags %}{{ tag }}{% if not loop.last %}, {% endif %}{% endfor %}
{% endif %}

{% if historical_tags and historical_tags|length > 0 %}

## User's Historical Tags (existing tags in the entire database):

{% for tag in historical_tags %}{{ tag }}{% if not loop.last %}, {% endif %}{% endfor %}
{% endif %}

Based on this note content and the user's tagging patterns, suggest 3-5 relevant tags that would help organize and categorize this note effectively. Respond only with a JSON array of strings, e.g. ["tag1", "tag2"].
25 changes: 24 additions & 1 deletion crates/template/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ pub enum PredefinedTemplate {
SuggestTagsUser,
#[strum(serialize = "ai_chat.system")]
AiChatSystem,
#[strum(serialize = "auto_generate_tags.system")]
AutoGenerateTagsSystem,
#[strum(serialize = "auto_generate_tags.user")]
AutoGenerateTagsUser,
}

impl From<PredefinedTemplate> for Template {
Expand All @@ -66,6 +70,12 @@ impl From<PredefinedTemplate> for Template {
Template::Static(PredefinedTemplate::SuggestTagsUser)
}
PredefinedTemplate::AiChatSystem => Template::Static(PredefinedTemplate::AiChatSystem),
PredefinedTemplate::AutoGenerateTagsSystem => {
Template::Static(PredefinedTemplate::AutoGenerateTagsSystem)
}
PredefinedTemplate::AutoGenerateTagsUser => {
Template::Static(PredefinedTemplate::AutoGenerateTagsUser)
}
}
}
}
Expand All @@ -77,6 +87,10 @@ pub const CREATE_TITLE_USER_TPL: &str = include_str!("../assets/create_title.use
pub const SUGGEST_TAGS_SYSTEM_TPL: &str = include_str!("../assets/suggest_tags.system.jinja");
pub const SUGGEST_TAGS_USER_TPL: &str = include_str!("../assets/suggest_tags.user.jinja");
pub const AI_CHAT_SYSTEM_TPL: &str = include_str!("../assets/ai_chat_system.jinja");
pub const AUTO_GENERATE_TAGS_SYSTEM_TPL: &str =
include_str!("../assets/auto_generate_tags.system.jinja");
pub const AUTO_GENERATE_TAGS_USER_TPL: &str =
include_str!("../assets/auto_generate_tags.user.jinja");

pub fn init(env: &mut minijinja::Environment) {
env.set_unknown_method_callback(minijinja_contrib::pycompat::unknown_method_callback);
Expand Down Expand Up @@ -113,7 +127,16 @@ pub fn init(env: &mut minijinja::Environment) {
AI_CHAT_SYSTEM_TPL,
)
.unwrap();

env.add_template(
PredefinedTemplate::AutoGenerateTagsSystem.as_ref(),
AUTO_GENERATE_TAGS_SYSTEM_TPL,
)
.unwrap();
env.add_template(
PredefinedTemplate::AutoGenerateTagsUser.as_ref(),
AUTO_GENERATE_TAGS_USER_TPL,
)
.unwrap();
env.add_filter("timeline", filters::timeline);
env.add_filter("language", filters::language);

Expand Down
Loading