diff --git a/apps/desktop/src/chat/context/support-block.ts b/apps/desktop/src/chat/context/support-block.ts index f6faba9f8e..ef3e34132c 100644 --- a/apps/desktop/src/chat/context/support-block.ts +++ b/apps/desktop/src/chat/context/support-block.ts @@ -2,6 +2,7 @@ import type { AccountInfo } from "@hypr/plugin-auth"; import { commands as authCommands } from "@hypr/plugin-auth"; import type { DeviceInfo } from "@hypr/plugin-misc"; import { commands as miscCommands } from "@hypr/plugin-misc"; +import type { ModelInfo } from "@hypr/plugin-template"; import { commands as templateCommands } from "@hypr/plugin-template"; import type { ContextEntity } from "./entities"; @@ -30,7 +31,9 @@ async function getDeviceInfo(): Promise { return null; } -export async function collectSupportContextBlock(): Promise<{ +export async function collectSupportContextBlock( + modelInfo?: ModelInfo | null, +): Promise<{ entities: ContextEntity[]; block: string | null; }> { @@ -54,7 +57,11 @@ export async function collectSupportContextBlock(): Promise<{ } const result = await templateCommands.renderSupport({ - supportContext: { account: accountInfo, device: deviceInfo }, + supportContext: { + account: accountInfo, + device: deviceInfo, + models: modelInfo ?? null, + }, }); return { diff --git a/apps/desktop/src/chat/mcp/useSupportMCP.ts b/apps/desktop/src/chat/mcp/useSupportMCP.ts index 97cd38a4a6..0e15e719a3 100644 --- a/apps/desktop/src/chat/mcp/useSupportMCP.ts +++ b/apps/desktop/src/chat/mcp/useSupportMCP.ts @@ -1,14 +1,51 @@ +import { useCallback } from "react"; + +import type { ModelInfo } from "@hypr/plugin-template"; + import { useMCP } from "./useMCP"; import { collectSupportContextBlock } from "~/chat/context/support-block"; +import { useConfigValues } from "~/shared/config"; export function useSupportMCP(enabled: boolean, accessToken?: string | null) { + const { + current_llm_provider, + current_llm_model, + current_stt_provider, + current_stt_model, + } = useConfigValues([ + "current_llm_provider", + "current_llm_model", + "current_stt_provider", + "current_stt_model", + ] as const); + + const modelInfo: ModelInfo | null = + current_llm_provider || current_stt_provider + ? { + llmProvider: current_llm_provider ?? null, + llmModel: current_llm_model ?? null, + sttProvider: current_stt_provider ?? null, + sttModel: current_stt_model ?? null, + } + : null; + + const collectContext = useCallback( + () => collectSupportContextBlock(modelInfo), + [ + current_llm_provider, + current_llm_model, + current_stt_provider, + current_stt_model, + ], + ); + return useMCP({ enabled, endpoint: "/support/mcp", clientName: "hyprnote-support-client", accessToken, promptName: "support_chat", - collectContext: collectSupportContextBlock, + collectContext, }); } diff --git a/crates/template-support/assets/support_chat.md.jinja b/crates/template-support/assets/support_chat.md.jinja index b5a292c187..e2a48fb102 100644 --- a/crates/template-support/assets/support_chat.md.jinja +++ b/crates/template-support/assets/support_chat.md.jinja @@ -17,6 +17,7 @@ Follow this workflow for bug reports and feature requests: 2. Call `search_issues` with relevant keywords to check for existing issues. 3. If a matching open issue exists, call `add_comment` with the user's additional context. 4. If no match exists, call `create_issue` with a clear title, structured body, and appropriate labels. +5. Always include the user's AI model configuration (LLM and STT provider/model) from the context in the issue body when available. For billing questions: diff --git a/crates/template-support/assets/support_context.md.jinja b/crates/template-support/assets/support_context.md.jinja index 2e10e9609b..f3cecc38b4 100644 --- a/crates/template-support/assets/support_context.md.jinja +++ b/crates/template-support/assets/support_context.md.jinja @@ -18,3 +18,22 @@ The following is automatically collected context about the current user and thei {%- if let Some(locale) = device.locale %} - Locale: {{ locale }} {%- endif %} + {%- if let Some(models) = models %} + {%- if models.llm_provider.is_some() || models.stt_provider.is_some() %} +- AI Models: + {%- if let Some(llm_provider) = models.llm_provider %} + {%- if let Some(llm_model) = models.llm_model %} + - LLM: {{ llm_provider }}/{{ llm_model }} + {%- else %} + - LLM: {{ llm_provider }} + {%- endif %} + {%- endif %} + {%- if let Some(stt_provider) = models.stt_provider %} + {%- if let Some(stt_model) = models.stt_model %} + - STT: {{ stt_provider }}/{{ stt_model }} + {%- else %} + - STT: {{ stt_provider }} + {%- endif %} + {%- endif %} + {%- endif %} + {%- endif %} diff --git a/crates/template-support/src/lib.rs b/crates/template-support/src/lib.rs index 1c6ff41739..38594a85af 100644 --- a/crates/template-support/src/lib.rs +++ b/crates/template-support/src/lib.rs @@ -21,6 +21,19 @@ pub struct DeviceInfo { pub locale: Option, } +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, specta::Type, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct ModelInfo { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub llm_provider: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub llm_model: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub stt_provider: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub stt_model: Option, +} + #[derive(askama::Template)] #[template(path = "bug_report.md.jinja", escape = "none")] struct BugReportBody<'a> { @@ -59,6 +72,7 @@ struct SupportChatPrompt; struct SupportContextBlock<'a> { account: Option<&'a AccountInfo>, device: &'a DeviceInfo, + models: Option<&'a ModelInfo>, } #[derive(Clone, serde::Deserialize, serde::Serialize, specta::Type)] @@ -75,6 +89,8 @@ pub enum SupportTemplate { pub struct SupportContext { pub account: Option, pub device: DeviceInfo, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub models: Option, } #[derive(Clone, serde::Deserialize, serde::Serialize, specta::Type)] @@ -111,6 +127,7 @@ pub fn render(t: SupportTemplate) -> Result { SupportTemplate::SupportContext(t) => askama::Template::render(&SupportContextBlock { account: t.account.as_ref(), device: &t.device, + models: t.models.as_ref(), }), SupportTemplate::BugReport(t) => askama::Template::render(&BugReportBody { description: &t.description, diff --git a/plugins/template/js/bindings.gen.ts b/plugins/template/js/bindings.gen.ts index 19df6f5bdc..a1d22fd86c 100644 --- a/plugins/template/js/bindings.gen.ts +++ b/plugins/template/js/bindings.gen.ts @@ -55,11 +55,12 @@ export type FeatureRequest = { description: string; platform: string; arch: stri export type Grammar = { task: "enhance"; sections: string[] | null } | { task: "title" } | { task: "tags" } | { task: "email-to-name" } export type JsonValue = null | boolean | number | string | JsonValue[] | Partial<{ [key in string]: JsonValue }> export type LogAnalysis = { summarySection: string; tail: string } +export type ModelInfo = { llmProvider?: string | null; llmModel?: string | null; sttProvider?: string | null; sttModel?: string | null } export type Participant = { name: string; jobTitle: string | null } export type Segment = { text: string; speaker: string } export type Session = { title: string | null; startedAt: string | null; endedAt: string | null; event: Event | null } export type SessionContext = { title: string | null; date: string | null; rawContent: string | null; enhancedContent: string | null; transcript: Transcript | null; participants: Participant[]; event: Event | null } -export type SupportContext = { account: AccountInfo | null; device: DeviceInfo } +export type SupportContext = { account: AccountInfo | null; device: DeviceInfo; models?: ModelInfo | null } export type SupportTemplate = { supportContext: SupportContext } | { bugReport: BugReport } | { featureRequest: FeatureRequest } | { logAnalysis: LogAnalysis } export type Template = { enhanceSystem: EnhanceSystem } | { enhanceUser: EnhanceUser } | { titleSystem: TitleSystem } | { titleUser: TitleUser } | { chatSystem: ChatSystem } | { contextBlock: ContextBlock } | { toolSearchSessions: ToolSearchSessions } export type TemplateSection = { title: string; description: string | null } diff --git a/plugins/tray/src/ext.rs b/plugins/tray/src/ext.rs index 1c46144f96..4111cd5493 100644 --- a/plugins/tray/src/ext.rs +++ b/plugins/tray/src/ext.rs @@ -128,7 +128,7 @@ impl<'a, M: tauri::Manager> Tray<'a, tauri::Wry, M> { Ok(()) } - pub fn set_title(&self, title: Option<&str>) -> Result<()> { + pub fn set_title(&self, title: Option<&str>) -> Result<()> { let app = self.manager.app_handle(); if let Some(tray) = app.tray_by_id(TRAY_ID) { tray.set_title(title)?; @@ -136,7 +136,6 @@ impl<'a, M: tauri::Manager> Tray<'a, tauri::Wry, M> { Ok(()) } - pub fn set_recording(&self, recording: bool) -> Result<()> { IS_RECORDING.store(recording, Ordering::SeqCst); Self::refresh_icon(self.manager.app_handle())