diff --git a/Cargo.lock b/Cargo.lock
index 7fc630c45..5b226916a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -7516,6 +7516,7 @@ dependencies = [
"async-openai",
"buffer",
"colored",
+ "data",
"dirs 6.0.0",
"encoding_rs",
"futures-util",
@@ -7523,9 +7524,14 @@ dependencies = [
"gbnf-validator",
"include_url_macro",
"indoc",
+ "listener-interface",
"llama-cpp-2",
+ "minijinja",
"serde",
+ "serde_json",
+ "template",
"thiserror 2.0.12",
+ "timeline",
"tokio",
]
@@ -13448,9 +13454,6 @@ dependencies = [
name = "tauri-plugin-template"
version = "0.1.0"
dependencies = [
- "codes-iso-639",
- "minijinja",
- "minijinja-contrib",
"serde",
"serde_json",
"specta",
@@ -13458,7 +13461,7 @@ dependencies = [
"tauri",
"tauri-plugin",
"tauri-specta",
- "thiserror 2.0.12",
+ "template",
"tracing",
]
@@ -13682,6 +13685,19 @@ dependencies = [
"windows-sys 0.59.0",
]
+[[package]]
+name = "template"
+version = "0.1.0"
+dependencies = [
+ "codes-iso-639",
+ "minijinja",
+ "minijinja-contrib",
+ "serde",
+ "serde_json",
+ "strum 0.26.3",
+ "thiserror 2.0.12",
+]
+
[[package]]
name = "tendril"
version = "0.4.3"
diff --git a/Cargo.toml b/Cargo.toml
index 9ae3d327a..49cdeb332 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -28,6 +28,7 @@ hypr-listener-interface = { path = "plugins/listener-interface", package = "list
hypr-llama = { path = "crates/llama", package = "llama" }
hypr-loops = { path = "crates/loops", package = "loops" }
hypr-nango = { path = "crates/nango", package = "nango" }
+hypr-template = { path = "crates/template", package = "template" }
hypr-timeline = { path = "crates/timeline", package = "timeline" }
hypr-turso = { path = "crates/turso", package = "turso" }
hypr-whisper = { path = "crates/whisper", package = "whisper" }
diff --git a/apps/desktop/src/components/editor-area/index.tsx b/apps/desktop/src/components/editor-area/index.tsx
index aa3ba1760..ac4a7a8ce 100644
--- a/apps/desktop/src/components/editor-area/index.tsx
+++ b/apps/desktop/src/components/editor-area/index.tsx
@@ -5,7 +5,6 @@ import { AnimatePresence } from "motion/react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useHypr } from "@/contexts";
-import { ENHANCE_SYSTEM_TEMPLATE_KEY, ENHANCE_USER_TEMPLATE_KEY } from "@/templates";
import { commands as analyticsCommands } from "@hypr/plugin-analytics";
import { commands as dbCommands } from "@hypr/plugin-db";
import { commands as miscCommands } from "@hypr/plugin-misc";
@@ -185,12 +184,12 @@ export function useEnhanceMutation({
const timeline = await fn(sessionId);
const systemMessage = await templateCommands.render(
- ENHANCE_SYSTEM_TEMPLATE_KEY,
+ "enhance.system",
{ config },
);
const userMessage = await templateCommands.render(
- ENHANCE_USER_TEMPLATE_KEY,
+ "enhance.user",
{
editor: rawContent,
timeline: timeline,
@@ -214,6 +213,7 @@ export function useEnhanceMutation({
for await (const chunk of textStream) {
setAnimate(true);
acc += chunk;
+ console.log("acc", acc);
const html = await miscCommands.opinionatedMdToHtml(acc);
setEnhancedContent(html);
}
diff --git a/apps/desktop/src/components/editor-area/note-header/listen-button/buttons.tsx b/apps/desktop/src/components/editor-area/note-header/listen-button/buttons.tsx
new file mode 100644
index 000000000..3da83e056
--- /dev/null
+++ b/apps/desktop/src/components/editor-area/note-header/listen-button/buttons.tsx
@@ -0,0 +1,90 @@
+import { Trans } from "@lingui/react/macro";
+import { PauseIcon } from "lucide-react";
+
+import SoundIndicator from "@/components/sound-indicator";
+import { Button } from "@hypr/ui/components/ui/button";
+import { PopoverTrigger } from "@hypr/ui/components/ui/popover";
+import { Spinner } from "@hypr/ui/components/ui/spinner";
+import { MicrophoneSelector } from "./microphone-selector";
+
+interface ButtonBaseProps {
+ disabled?: boolean;
+ onClick: () => void;
+ isEnhanced?: boolean;
+}
+
+export const LoadingButton = () => {
+ return (
+
+
+
+ );
+};
+
+export const ResumeButton = ({
+ disabled,
+ onClick,
+ isEnhanced,
+}: ButtonBaseProps) => {
+ return (
+
+
+
+ );
+};
+
+export const InitialRecordButton = ({ disabled, onClick }: ButtonBaseProps) => {
+ return (
+
+
+
+ );
+};
+
+export const ActiveRecordButton = ({ onClick }: ButtonBaseProps) => {
+ return (
+
+
+
+
+
+ );
+};
+
+export const StopRecordingButton = ({ onClick }: { onClick: () => void }) => {
+ return (
+
+ );
+};
diff --git a/apps/desktop/src/components/editor-area/note-header/listen-button.tsx b/apps/desktop/src/components/editor-area/note-header/listen-button/index.tsx
similarity index 60%
rename from apps/desktop/src/components/editor-area/note-header/listen-button.tsx
rename to apps/desktop/src/components/editor-area/note-header/listen-button/index.tsx
index 94eafa832..71744be57 100644
--- a/apps/desktop/src/components/editor-area/note-header/listen-button.tsx
+++ b/apps/desktop/src/components/editor-area/note-header/listen-button/index.tsx
@@ -1,17 +1,15 @@
-import { Trans } from "@lingui/react/macro";
import { useMutation, useQuery } from "@tanstack/react-query";
-import { MicIcon, MicOffIcon, PauseIcon, Volume2Icon, VolumeOffIcon } from "lucide-react";
+import { MicIcon, MicOffIcon, Volume2Icon, VolumeOffIcon } from "lucide-react";
import { useEffect, useState } from "react";
import SoundIndicator from "@/components/sound-indicator";
import { commands as listenerCommands } from "@hypr/plugin-listener";
import { commands as localSttCommands } from "@hypr/plugin-local-stt";
import { Button } from "@hypr/ui/components/ui/button";
-import { Popover, PopoverContent, PopoverTrigger } from "@hypr/ui/components/ui/popover";
-import { Spinner } from "@hypr/ui/components/ui/spinner";
+import { Popover, PopoverContent } from "@hypr/ui/components/ui/popover";
import { toast } from "@hypr/ui/components/ui/toast";
-import { Tooltip, TooltipContent, TooltipTrigger } from "@hypr/ui/components/ui/tooltip";
import { useOngoingSession, useSession } from "@hypr/utils/contexts";
+import { ActiveRecordButton, InitialRecordButton, LoadingButton, ResumeButton, StopRecordingButton } from "./buttons";
interface ListenButtonProps {
sessionId: string;
@@ -20,6 +18,14 @@ interface ListenButtonProps {
export default function ListenButton({ sessionId }: ListenButtonProps) {
const [open, setOpen] = useState(false);
+ const ongoingSessionStore = useOngoingSession((s) => ({
+ start: s.start,
+ pause: s.pause,
+ isCurrent: s.sessionId === sessionId,
+ status: s.status,
+ timeline: s.timeline,
+ }));
+
const modelDownloaded = useQuery({
queryKey: ["check-stt-model-downloaded"],
refetchInterval: 1000,
@@ -32,14 +38,6 @@ export default function ListenButton({ sessionId }: ListenButtonProps) {
},
});
- const ongoingSessionStore = useOngoingSession((s) => ({
- start: s.start,
- pause: s.pause,
- isCurrent: s.sessionId === sessionId,
- status: s.status,
- timeline: s.timeline,
- }));
-
const startedBefore = useSession(
sessionId,
(s) => s.session.conversations.length > 0,
@@ -119,70 +117,26 @@ export default function ListenButton({ sessionId }: ListenButtonProps) {
return (
<>
- {ongoingSessionStore.status === "loading" && (
-
-
-
- )}
+ {ongoingSessionStore.status === "loading" && }
+
{showResumeButton && (
-
+ isEnhanced={!!isEnhanced}
+ />
)}
+
{ongoingSessionStore.status === "inactive" && !showResumeButton && (
-
-
-
-
-
-
- Start recording
-
-
-
+
)}
{ongoingSessionStore.status === "active" && (
-
-
-
-
-
-
-
-
- Pause recording
-
-
-
+
@@ -198,14 +152,7 @@ export default function ListenButton({ sessionId }: ListenButtonProps) {
/>
-
+
)}
diff --git a/apps/desktop/src/components/editor-area/note-header/listen-button/microphone-selector.tsx b/apps/desktop/src/components/editor-area/note-header/listen-button/microphone-selector.tsx
new file mode 100644
index 000000000..95fa84140
--- /dev/null
+++ b/apps/desktop/src/components/editor-area/note-header/listen-button/microphone-selector.tsx
@@ -0,0 +1,70 @@
+import { Trans } from "@lingui/react/macro";
+import React, { useState } from "react";
+
+import { HoverCard, HoverCardContent, HoverCardTrigger } from "@hypr/ui/components/ui/hover-card";
+
+// TODO
+export const useMicrophones = () => {
+ const [selectedMic, setSelectedMic] = useState("default");
+
+ // Mock data for available microphones
+ const microphones = [
+ { id: "default", name: "System Default" },
+ { id: "headset", name: "Logitech G Pro X Headset" },
+ { id: "webcam", name: "Logitech C920 Webcam" },
+ { id: "usb", name: "Blue Yeti USB Microphone" },
+ { id: "bluetooth", name: "AirPods Pro" },
+ ];
+
+ return {
+ microphones,
+ selectedMic,
+ setSelectedMic,
+ isLoading: false,
+ error: null,
+ };
+};
+
+export const MicrophoneSelector = ({
+ children,
+}: {
+ children: React.ReactNode;
+}) => {
+ const { microphones, selectedMic, setSelectedMic } = useMicrophones();
+
+ return (
+
+ {children}
+
+
+
+ Select Microphone
+
+
+
+ {microphones.map((mic) => (
+
setSelectedMic(mic.id)}
+ >
+
+ {selectedMic === mic.id &&
}
+
+
+
+ ))}
+
+
+
+
+ );
+};
diff --git a/apps/desktop/src/locales/en/messages.po b/apps/desktop/src/locales/en/messages.po
index 8c62f8848..4bb830a46 100644
--- a/apps/desktop/src/locales/en/messages.po
+++ b/apps/desktop/src/locales/en/messages.po
@@ -702,8 +702,7 @@ msgstr "Optional for participant suggestions"
msgid "Owner"
msgstr "Owner"
-#: src/components/editor-area/note-header/listen-button.tsx:182
-#: src/components/editor-area/note-header/listen-button.tsx:207
+#: src/components/editor-area/note-header/listen-button/buttons.tsx:87
msgid "Pause recording"
msgstr "Pause recording"
@@ -761,7 +760,7 @@ msgstr "Required to transcribe other people's voice during meetings"
msgid "Required to transcribe your voice during meetings"
msgstr "Required to transcribe your voice during meetings"
-#: src/components/editor-area/note-header/listen-button.tsx:140
+#: src/components/editor-area/note-header/listen-button/buttons.tsx:43
msgid "Resume"
msgstr "Resume"
@@ -819,6 +818,10 @@ msgstr "Select apps that should not trigger meeting notifications"
msgid "Select Calendars"
msgstr "Select Calendars"
+#: src/components/editor-area/note-header/listen-button/microphone-selector.tsx:41
+msgid "Select Microphone"
+msgstr "Select Microphone"
+
#: src/components/settings/views/team.tsx:240
msgid "Send invite"
msgstr "Send invite"
@@ -856,8 +859,8 @@ msgid "Start Monthly Plan"
msgstr "Start Monthly Plan"
#: src/components/editor-area/note-header/listen-button.tsx:158
-msgid "Start recording"
-msgstr "Start recording"
+#~ msgid "Start recording"
+#~ msgstr "Start recording"
#: src/components/settings/views/ai.tsx:199
#: src/components/settings/views/ai.tsx:373
diff --git a/apps/desktop/src/locales/ko/messages.po b/apps/desktop/src/locales/ko/messages.po
index ab1a9250a..ea61f6889 100644
--- a/apps/desktop/src/locales/ko/messages.po
+++ b/apps/desktop/src/locales/ko/messages.po
@@ -702,8 +702,7 @@ msgstr ""
msgid "Owner"
msgstr ""
-#: src/components/editor-area/note-header/listen-button.tsx:182
-#: src/components/editor-area/note-header/listen-button.tsx:207
+#: src/components/editor-area/note-header/listen-button/buttons.tsx:87
msgid "Pause recording"
msgstr ""
@@ -761,7 +760,7 @@ msgstr ""
msgid "Required to transcribe your voice during meetings"
msgstr ""
-#: src/components/editor-area/note-header/listen-button.tsx:140
+#: src/components/editor-area/note-header/listen-button/buttons.tsx:43
msgid "Resume"
msgstr ""
@@ -819,6 +818,10 @@ msgstr ""
msgid "Select Calendars"
msgstr ""
+#: src/components/editor-area/note-header/listen-button/microphone-selector.tsx:41
+msgid "Select Microphone"
+msgstr ""
+
#: src/components/settings/views/team.tsx:240
msgid "Send invite"
msgstr ""
@@ -856,8 +859,8 @@ msgid "Start Monthly Plan"
msgstr ""
#: src/components/editor-area/note-header/listen-button.tsx:158
-msgid "Start recording"
-msgstr ""
+#~ msgid "Start recording"
+#~ msgstr ""
#: src/components/settings/views/ai.tsx:199
#: src/components/settings/views/ai.tsx:373
diff --git a/apps/desktop/src/routes/app.tsx b/apps/desktop/src/routes/app.tsx
index ac6802c0e..ac526d722 100644
--- a/apps/desktop/src/routes/app.tsx
+++ b/apps/desktop/src/routes/app.tsx
@@ -18,7 +18,6 @@ import {
useLeftSidebar,
useRightPanel,
} from "@/contexts";
-import { registerTemplates } from "@/templates";
import { commands } from "@/types";
import { commands as listenerCommands } from "@hypr/plugin-listener";
import { events as windowsEvents, getCurrentWebviewWindowLabel } from "@hypr/plugin-windows";
@@ -36,10 +35,6 @@ function Component() {
const router = useRouter();
const { sessionsStore, isOnboardingNeeded } = Route.useLoaderData();
- useEffect(() => {
- registerTemplates();
- }, []);
-
const windowLabel = getCurrentWebviewWindowLabel();
const showNotifications = windowLabel === "main" && !isOnboardingNeeded;
diff --git a/apps/desktop/src/templates/index.ts b/apps/desktop/src/templates/index.ts
deleted file mode 100644
index ea2e4dcf5..000000000
--- a/apps/desktop/src/templates/index.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import ENHANCE_SYSTEM_TEMPLATE_VALUE from "./enhance.system.jinja?raw";
-import ENHANCE_USER_TEMPLATE_VALUE from "./enhance.user.jinja?raw";
-
-export const ENHANCE_SYSTEM_TEMPLATE_KEY = "enhance-system";
-export const ENHANCE_USER_TEMPLATE_KEY = "enhance-user";
-
-import { commands as templateCommands } from "@hypr/plugin-template";
-
-export const registerTemplates = () => {
- return Promise.all([
- templateCommands.registerTemplate(
- ENHANCE_SYSTEM_TEMPLATE_KEY,
- ENHANCE_SYSTEM_TEMPLATE_VALUE,
- ),
- templateCommands.registerTemplate(
- ENHANCE_USER_TEMPLATE_KEY,
- ENHANCE_USER_TEMPLATE_VALUE,
- ),
- ]);
-};
diff --git a/apps/desktop/src/templates/types.d.ts b/apps/desktop/src/templates/types.d.ts
deleted file mode 100644
index 8c27cc1c1..000000000
--- a/apps/desktop/src/templates/types.d.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-declare module "*.jinja?raw" {
- const content: string;
- export default content;
-}
diff --git a/apps/docs/data/i18n.json b/apps/docs/data/i18n.json
index 5425aa364..00c6b9cc7 100644
--- a/apps/docs/data/i18n.json
+++ b/apps/docs/data/i18n.json
@@ -1,12 +1,12 @@
[
{
"language": "ko",
- "total": 232,
- "missing": 224
+ "total": 233,
+ "missing": 225
},
{
"language": "en (source)",
- "total": 232,
+ "total": 233,
"missing": 0
}
]
diff --git a/crates/llama/Cargo.toml b/crates/llama/Cargo.toml
index a3c3e2203..3eebaa7d8 100644
--- a/crates/llama/Cargo.toml
+++ b/crates/llama/Cargo.toml
@@ -4,12 +4,18 @@ version = "0.1.0"
edition = "2021"
[dev-dependencies]
-gbnf-validator = { git = "https://github.com/fastrepl/gbnf-validator", rev = "3dec055" }
hypr-buffer = { workspace = true }
+hypr-data = { workspace = true }
+hypr-listener-interface = { workspace = true }
+hypr-template = { workspace = true }
+hypr-timeline = { workspace = true }
colored = "3.0.0"
dirs = { workspace = true }
+gbnf-validator = { git = "https://github.com/fastrepl/gbnf-validator", rev = "3dec055" }
indoc = "2.0.6"
+minijinja = { workspace = true }
+serde_json = { workspace = true }
[dependencies]
include_url_macro = { workspace = true }
diff --git a/crates/llama/src/grammar/markdown.gbnf b/crates/llama/src/grammar/markdown.gbnf
index f0ed1cbbc..df20a0473 100644
--- a/crates/llama/src/grammar/markdown.gbnf
+++ b/crates/llama/src/grammar/markdown.gbnf
@@ -1,5 +1,8 @@
root ::= section+
section ::= header "\n\n" content
-header ::= "# " title
-title ::= [^#\n]+
-content ::= [^#] [^#]*
+header ::= "# " char+
+content ::= [^#[^*\n] [^#`[^=]*
+
+char ::= letter | digit | " "
+letter ::= [a-zA-Z]
+digit ::= [0-9]
\ No newline at end of file
diff --git a/crates/llama/src/grammar/mod.rs b/crates/llama/src/grammar/mod.rs
index 3afaec10e..7613d5f42 100644
--- a/crates/llama/src/grammar/mod.rs
+++ b/crates/llama/src/grammar/mod.rs
@@ -17,6 +17,7 @@ mod tests {
struct TestCase {
pub grammar: &'static str,
+ pub intent: &'static str,
pub text: &'static str,
pub valid: bool,
pub debug: bool,
@@ -27,6 +28,7 @@ mod tests {
let test_cases = vec![
TestCase {
grammar: MARKDOWN_GRAMMAR,
+ intent: "should start with a heading",
text: indoc::indoc! {r#"
Here's a response:
@@ -36,11 +38,45 @@ mod tests {
},
TestCase {
grammar: MARKDOWN_GRAMMAR,
+ intent: "header-content pair is required",
text: indoc::indoc! {r#"
# This is a test
- 12"#},
- valid: true,
+ ## This is a test"#},
+ valid: false,
+ debug: false,
+ },
+ TestCase {
+ grammar: MARKDOWN_GRAMMAR,
+ intent: "footnote is not allowed. Currently we prevent both '[' and '^' to achieve this.",
+ text: indoc::indoc! {r#"
+ # This is a test
+
+ Hi Hello. [^1]"#},
+ valid: false,
+ debug: false,
+ },
+ TestCase {
+ grammar: MARKDOWN_GRAMMAR,
+ intent: "codeblock is not allowed. Currently we prevent '`' to achieve this.",
+ text: indoc::indoc! {r#"
+ # This is a test
+
+ Hi Hello. `code`.
+ ```code
+ 123
+ ```"#},
+ valid: false,
+ debug: false,
+ },
+ TestCase {
+ grammar: MARKDOWN_GRAMMAR,
+ intent: "content should not start with bold text. we prevent '*'(but allow '-') to achieve this.",
+ text: indoc::indoc! {r#"
+ # Enhanced Meeting Notes
+
+ **What Hyprnote Does**"#},
+ valid: false,
debug: false,
},
];
@@ -53,7 +89,7 @@ mod tests {
Ok(valid_actual) => {
if valid_actual != test_case.valid {
println!("{}", "=".repeat(80));
- println!("{}_failed", i);
+ println!("{}_failed. intent: {}", i, test_case.intent);
debug_grammar_failure_point(&gbnf, test_case.grammar, test_case.text);
println!("\n{}", "=".repeat(80));
@@ -62,7 +98,7 @@ mod tests {
if test_case.debug {
println!("{}", "=".repeat(80));
- println!("{}_passed", i);
+ println!("{}_passed. intent: {}", i, test_case.intent);
debug_grammar_failure_point(&gbnf, test_case.grammar, test_case.text);
println!("\n{}", "=".repeat(80));
}
diff --git a/crates/llama/src/lib.rs b/crates/llama/src/lib.rs
index c5b2c4596..40d956b4e 100644
--- a/crates/llama/src/lib.rs
+++ b/crates/llama/src/lib.rs
@@ -19,7 +19,7 @@ pub use message::*;
const TEMPLATE_NAME: &str = "llama3";
const DEFAULT_MAX_INPUT_TOKENS: u32 = 1024 * 8;
-const DEFAULT_MAX_OUTPUT_TOKENS: u32 = 1024 * 2;
+const DEFAULT_MAX_OUTPUT_TOKENS: u32 = 1024;
static LLAMA_BACKEND: OnceLock> = OnceLock::new();
@@ -99,8 +99,8 @@ impl Llama {
let mut sampler = LlamaSampler::chain_simple([
LlamaSampler::grammar(&model, grammar::MARKDOWN_GRAMMAR, "root"),
LlamaSampler::temp(0.5),
- LlamaSampler::penalties(0, 1.5, 0.2, 0.0),
- LlamaSampler::dist(1234),
+ LlamaSampler::penalties(0, 1.2, 0.2, 0.0),
+ LlamaSampler::mirostat_v2(1234, 4.0, 0.1),
]);
while n_cur <= last_index + DEFAULT_MAX_OUTPUT_TOKENS as i32 {
@@ -170,23 +170,74 @@ mod tests {
.unwrap()
.join("com.hyprnote.dev")
.join("llm.gguf");
+
Llama::new(model_path).unwrap()
}
- // cargo test test_simple -p llama -- --nocapture
- #[tokio::test]
- async fn test_simple() {
- use futures_util::pin_mut;
- use std::io::{self, Write};
+ fn english_4_messages() -> Vec {
+ let timeline_view = {
+ let (transcripts, diarizations): (
+ Vec,
+ Vec,
+ ) = (
+ serde_json::from_str(hypr_data::english_4::TRANSCRIPTION_JSON).unwrap(),
+ serde_json::from_str(hypr_data::english_4::DIARIZATION_JSON).unwrap(),
+ );
- let llama = get_model();
- let prompt = "Generate a random meeting summary note.";
+ let mut timeline = hypr_timeline::Timeline::default();
+
+ for t in transcripts {
+ timeline.add_transcription(t);
+ }
+ for d in diarizations {
+ timeline.add_diarization(d);
+ }
+
+ timeline.view(hypr_timeline::TimelineFilter::default())
+ };
+
+ let mut env = hypr_template::minijinja::Environment::new();
+ hypr_template::init(&mut env);
+
+ let system = hypr_template::render(
+ &env,
+ hypr_template::PredefinedTemplate::EnhanceSystem.into(),
+ &serde_json::json!({
+ "config": {
+ "general": {
+ "display_language": "en"
+ }
+ }
+ })
+ .as_object()
+ .unwrap(),
+ )
+ .unwrap();
+
+ let user = hypr_template::render(
+ &env,
+ hypr_template::PredefinedTemplate::EnhanceUser.into(),
+ &serde_json::json!({
+ "editor": "privacy aspect seems interesting",
+ "timeline": timeline_view,
+ "participants": vec!["yujonglee".to_string()],
+ })
+ .as_object()
+ .unwrap(),
+ )
+ .unwrap();
+
+ vec![
+ LlamaChatMessage::new("system".into(), system.into()).unwrap(),
+ LlamaChatMessage::new("user".into(), user.into()).unwrap(),
+ ]
+ }
- let request = LlamaRequest::new(vec![
- LlamaChatMessage::new("user".into(), prompt.into()).unwrap()
- ]);
+ async fn print_stream(model: &Llama, request: LlamaRequest) {
+ use futures_util::pin_mut;
+ use std::io::{self, Write};
- let stream = llama.generate_stream(request).unwrap();
+ let stream = model.generate_stream(request).unwrap();
pin_mut!(stream);
while let Some(token) = stream.next().await {
@@ -195,4 +246,13 @@ mod tests {
}
println!();
}
+
+ // cargo test test_english_4 -p llama -- --nocapture
+ #[tokio::test]
+ async fn test_english_4() {
+ let llama = get_model();
+ let request = LlamaRequest::new(english_4_messages());
+
+ print_stream(&llama, request).await;
+ }
}
diff --git a/crates/template/Cargo.toml b/crates/template/Cargo.toml
new file mode 100644
index 000000000..86bfe0030
--- /dev/null
+++ b/crates/template/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "template"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+codes-iso-639 = { workspace = true }
+serde = { workspace = true }
+serde_json = { workspace = true }
+strum = { workspace = true, features = ["derive"] }
+thiserror = { workspace = true }
+
+minijinja = { workspace = true, features = ["loader", "preserve_order", "json"] }
+minijinja-contrib = { workspace = true, features = ["pycompat"] }
diff --git a/apps/desktop/src/templates/ai_chat.system.jinja b/crates/template/assets/ai_chat.system.jinja
similarity index 100%
rename from apps/desktop/src/templates/ai_chat.system.jinja
rename to crates/template/assets/ai_chat.system.jinja
diff --git a/apps/desktop/src/templates/create_title.system.jinja b/crates/template/assets/create_title.system.jinja
similarity index 100%
rename from apps/desktop/src/templates/create_title.system.jinja
rename to crates/template/assets/create_title.system.jinja
diff --git a/apps/desktop/src/templates/create_title.user.jinja b/crates/template/assets/create_title.user.jinja
similarity index 100%
rename from apps/desktop/src/templates/create_title.user.jinja
rename to crates/template/assets/create_title.user.jinja
diff --git a/apps/desktop/src/templates/enhance.system.jinja b/crates/template/assets/enhance.system.jinja
similarity index 100%
rename from apps/desktop/src/templates/enhance.system.jinja
rename to crates/template/assets/enhance.system.jinja
diff --git a/apps/desktop/src/templates/enhance.user.jinja b/crates/template/assets/enhance.user.jinja
similarity index 100%
rename from apps/desktop/src/templates/enhance.user.jinja
rename to crates/template/assets/enhance.user.jinja
diff --git a/apps/desktop/src/templates/postprocess_enhance.system.jinja b/crates/template/assets/postprocess_enhance.system.jinja
similarity index 100%
rename from apps/desktop/src/templates/postprocess_enhance.system.jinja
rename to crates/template/assets/postprocess_enhance.system.jinja
diff --git a/apps/desktop/src/templates/postprocess_enhance.user.jinja b/crates/template/assets/postprocess_enhance.user.jinja
similarity index 100%
rename from apps/desktop/src/templates/postprocess_enhance.user.jinja
rename to crates/template/assets/postprocess_enhance.user.jinja
diff --git a/apps/desktop/src/templates/show_annotation.system.jinja b/crates/template/assets/show_annotation.system.jinja
similarity index 100%
rename from apps/desktop/src/templates/show_annotation.system.jinja
rename to crates/template/assets/show_annotation.system.jinja
diff --git a/apps/desktop/src/templates/show_annotation.user.jinja b/crates/template/assets/show_annotation.user.jinja
similarity index 100%
rename from apps/desktop/src/templates/show_annotation.user.jinja
rename to crates/template/assets/show_annotation.user.jinja
diff --git a/crates/template/src/error.rs b/crates/template/src/error.rs
new file mode 100644
index 000000000..b6e3a9b6f
--- /dev/null
+++ b/crates/template/src/error.rs
@@ -0,0 +1,16 @@
+use serde::{ser::Serializer, Serialize};
+
+#[derive(Debug, thiserror::Error)]
+pub enum Error {
+ #[error(transparent)]
+ JinjaError(#[from] minijinja::Error),
+}
+
+impl Serialize for Error {
+ fn serialize(&self, serializer: S) -> std::result::Result
+ where
+ S: Serializer,
+ {
+ serializer.serialize_str(self.to_string().as_ref())
+ }
+}
diff --git a/crates/template/src/lib.rs b/crates/template/src/lib.rs
new file mode 100644
index 000000000..027109c4b
--- /dev/null
+++ b/crates/template/src/lib.rs
@@ -0,0 +1,102 @@
+use codes_iso_639::part_1::LanguageCode;
+
+mod error;
+pub use error::*;
+
+pub use minijinja;
+
+pub enum Template {
+ Static(PredefinedTemplate),
+ Dynamic(String),
+}
+
+impl From for Template {
+ fn from(value: String) -> Self {
+ Template::Dynamic(value)
+ }
+}
+
+impl From for String {
+ fn from(value: Template) -> Self {
+ match value {
+ Template::Static(t) => t.to_string(),
+ Template::Dynamic(t) => t,
+ }
+ }
+}
+
+#[derive(Debug, strum::AsRefStr, strum::Display)]
+pub enum PredefinedTemplate {
+ #[strum(serialize = "enhance.system")]
+ EnhanceSystem,
+ #[strum(serialize = "enhance.user")]
+ EnhanceUser,
+}
+
+impl From for Template {
+ fn from(value: PredefinedTemplate) -> Self {
+ match value {
+ PredefinedTemplate::EnhanceSystem => {
+ Template::Static(PredefinedTemplate::EnhanceSystem)
+ }
+ PredefinedTemplate::EnhanceUser => Template::Static(PredefinedTemplate::EnhanceUser),
+ }
+ }
+}
+
+const ENHANCE_SYSTEM: &str = include_str!("../assets/enhance.system.jinja");
+const ENHANCE_USER: &str = include_str!("../assets/enhance.user.jinja");
+
+pub fn init(env: &mut minijinja::Environment) {
+ env.set_unknown_method_callback(minijinja_contrib::pycompat::unknown_method_callback);
+
+ env.add_template(PredefinedTemplate::EnhanceSystem.as_ref(), ENHANCE_SYSTEM)
+ .unwrap();
+ env.add_template(PredefinedTemplate::EnhanceUser.as_ref(), ENHANCE_USER)
+ .unwrap();
+
+ env.add_filter("language", filters::language);
+
+ [LanguageCode::En, LanguageCode::Ko]
+ .iter()
+ .for_each(|lang| {
+ env.add_test(
+ lang.language_name().to_lowercase(),
+ testers::language(*lang),
+ );
+ });
+}
+
+pub fn render(
+ env: &minijinja::Environment<'static>,
+ template: Template,
+ ctx: &serde_json::Map,
+) -> Result {
+ let tpl = match template {
+ Template::Static(t) => env.get_template(t.as_ref())?,
+ Template::Dynamic(t) => env.get_template(&t)?,
+ };
+
+ tpl.render(ctx).map_err(Into::into)
+}
+
+// https://docs.rs/minijinja/latest/minijinja/filters/trait.Filter.html
+mod filters {
+ use codes_iso_639::part_1::LanguageCode;
+ use std::str::FromStr;
+
+ pub fn language(value: String) -> String {
+ let lang_str = value.to_lowercase();
+ let lang_code = LanguageCode::from_str(&lang_str).unwrap();
+ lang_code.language_name().to_string()
+ }
+}
+
+// https://docs.rs/minijinja/latest/minijinja/tests/index.html
+mod testers {
+ use codes_iso_639::part_1::LanguageCode;
+
+ pub fn language(lang: LanguageCode) -> impl minijinja::tests::Test {
+ move |value: String| value.to_lowercase() == lang.code().to_lowercase()
+ }
+}
diff --git a/packages/tiptap/src/shared/extensions.ts b/packages/tiptap/src/shared/extensions.ts
index 96a056fc3..e6bc87292 100644
--- a/packages/tiptap/src/shared/extensions.ts
+++ b/packages/tiptap/src/shared/extensions.ts
@@ -49,7 +49,7 @@ export const extensions = [
return false;
}
- const disallowedProtocols = ["ftp", "file", "mailto"];
+ const disallowedProtocols = ["ftp", "file"];
const protocol = parsedUrl.protocol.replace(":", "");
if (disallowedProtocols.includes(protocol)) {
@@ -62,13 +62,6 @@ export const extensions = [
return false;
}
- const disallowedDomains = ["example-phishing.com", "malicious-site.net"];
- const domain = parsedUrl.hostname;
-
- if (disallowedDomains.includes(domain)) {
- return false;
- }
-
return true;
} catch {
return false;
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 28e430966..b8aaa8c19 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -23,6 +23,7 @@
"@radix-ui/react-context-menu": "^2.2.7",
"@radix-ui/react-dialog": "^1.1.7",
"@radix-ui/react-dropdown-menu": "^2.1.7",
+ "@radix-ui/react-hover-card": "^1.1.7",
"@radix-ui/react-label": "^2.1.3",
"@radix-ui/react-popover": "^1.1.7",
"@radix-ui/react-progress": "^1.1.3",
diff --git a/packages/ui/src/components/ui/hover-card.tsx b/packages/ui/src/components/ui/hover-card.tsx
new file mode 100644
index 000000000..890f29892
--- /dev/null
+++ b/packages/ui/src/components/ui/hover-card.tsx
@@ -0,0 +1,29 @@
+"use client";
+
+import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
+import * as React from "react";
+
+import { cn } from "@hypr/ui/lib/utils";
+
+const HoverCard = HoverCardPrimitive.Root;
+
+const HoverCardTrigger = HoverCardPrimitive.Trigger;
+
+const HoverCardContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
+
+));
+HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
+
+export { HoverCard, HoverCardContent, HoverCardTrigger };
diff --git a/plugins/template/Cargo.toml b/plugins/template/Cargo.toml
index 6273a1d1b..fe0d29b94 100644
--- a/plugins/template/Cargo.toml
+++ b/plugins/template/Cargo.toml
@@ -14,16 +14,12 @@ tauri-plugin = { workspace = true, features = ["build"] }
specta-typescript = { workspace = true }
[dependencies]
+hypr-template = { workspace = true }
+
serde = { workspace = true }
serde_json = { workspace = true }
-specta = { workspace = true, features = ["serde_json"] }
-
-codes-iso-639 = { workspace = true }
-thiserror = { workspace = true }
tracing = { workspace = true }
+specta = { workspace = true, features = ["serde_json"] }
tauri = { workspace = true, features = ["test"] }
tauri-specta = { workspace = true, features = ["derive", "typescript"] }
-
-minijinja = { workspace = true, features = ["loader", "preserve_order", "json"] }
-minijinja-contrib = { workspace = true, features = ["pycompat"] }
diff --git a/plugins/template/src/engine.rs b/plugins/template/src/engine.rs
deleted file mode 100644
index f5abf0c7c..000000000
--- a/plugins/template/src/engine.rs
+++ /dev/null
@@ -1,37 +0,0 @@
-use codes_iso_639::part_1::LanguageCode;
-
-pub fn init(env: &mut minijinja::Environment) {
- env.set_unknown_method_callback(minijinja_contrib::pycompat::unknown_method_callback);
-
- env.add_filter("language", filters::language);
-
- [LanguageCode::En, LanguageCode::Ko]
- .iter()
- .for_each(|lang| {
- env.add_test(
- lang.language_name().to_lowercase(),
- testers::language(*lang),
- );
- });
-}
-
-// https://docs.rs/minijinja/latest/minijinja/filters/trait.Filter.html
-mod filters {
- use codes_iso_639::part_1::LanguageCode;
- use std::str::FromStr;
-
- pub fn language(value: String) -> String {
- let lang_str = value.to_lowercase();
- let lang_code = LanguageCode::from_str(&lang_str).unwrap();
- lang_code.language_name().to_string()
- }
-}
-
-// https://docs.rs/minijinja/latest/minijinja/tests/index.html
-mod testers {
- use codes_iso_639::part_1::LanguageCode;
-
- pub fn language(lang: LanguageCode) -> impl minijinja::tests::Test {
- move |value: String| value.to_lowercase() == lang.code().to_lowercase()
- }
-}
diff --git a/plugins/template/src/ext.rs b/plugins/template/src/ext.rs
index de4e583ce..34a3aa2c2 100644
--- a/plugins/template/src/ext.rs
+++ b/plugins/template/src/ext.rs
@@ -1,7 +1,7 @@
pub trait TemplatePluginExt {
fn render(
&self,
- name: impl AsRef,
+ name: impl Into,
ctx: serde_json::Map,
) -> Result;
fn register_template(
@@ -15,19 +15,18 @@ impl> crate::TemplatePluginExt for T
#[tracing::instrument(skip_all)]
fn render(
&self,
- name: impl AsRef,
+ name: impl Into,
ctx: serde_json::Map,
) -> Result {
let state = self.state::();
- let s = state.lock().unwrap();
- let tpl = s
- .env
- .get_template(name.as_ref())
- .map_err(|e| e.to_string())?;
- tpl.render(&ctx)
- .map(|s| s.trim().to_string())
- .map_err(|e| e.to_string())
+ {
+ let guard = state.lock().unwrap();
+
+ hypr_template::render(&guard.env, name.into(), &ctx)
+ .map(|s| s.trim().to_string())
+ .map_err(|e| e.to_string())
+ }
}
#[tracing::instrument(skip_all)]
@@ -37,10 +36,13 @@ impl> crate::TemplatePluginExt for T
template: impl Into,
) -> Result<(), String> {
let state = self.state::();
- let mut state = state.lock().unwrap();
- state
- .env
- .add_template_owned(name.into(), template.into())
- .map_err(|e| e.to_string())
+
+ {
+ let mut guard = state.lock().unwrap();
+ guard
+ .env
+ .add_template_owned(name.into(), template.into())
+ .map_err(|e| e.to_string())
+ }
}
}
diff --git a/plugins/template/src/lib.rs b/plugins/template/src/lib.rs
index 00261c5e5..e252ff9a7 100644
--- a/plugins/template/src/lib.rs
+++ b/plugins/template/src/lib.rs
@@ -2,7 +2,6 @@ use std::sync::Mutex;
use tauri::{Manager, Wry};
mod commands;
-mod engine;
mod ext;
pub use ext::TemplatePluginExt;
@@ -12,13 +11,13 @@ const PLUGIN_NAME: &str = "template";
pub type ManagedState = Mutex;
pub struct State {
- env: minijinja::Environment<'static>,
+ env: hypr_template::minijinja::Environment<'static>,
}
impl Default for State {
fn default() -> Self {
Self {
- env: minijinja::Environment::new(),
+ env: hypr_template::minijinja::Environment::new(),
}
}
}
@@ -40,7 +39,7 @@ pub fn init() -> tauri::plugin::TauriPlugin {
.invoke_handler(specta_builder.invoke_handler())
.setup(|app, _api| {
let mut state = State::default();
- engine::init(&mut state.env);
+ hypr_template::init(&mut state.env);
app.manage(Mutex::new(state));
Ok(())
})
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 2465159a3..d893f8d49 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -966,7 +966,7 @@ importers:
dependencies:
'@remixicon/react':
specifier: ^4.6.0
- version: 4.6.0(react@19.0.0)
+ version: 4.6.0(react@18.3.1)
'@tiptap/core':
specifier: ^2.11.7
version: 2.11.7(@tiptap/pm@2.11.7)
@@ -1005,7 +1005,7 @@ importers:
version: 2.11.7
'@tiptap/react':
specifier: ^2.11.7
- version: 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ version: 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@tiptap/starter-kit':
specifier: ^2.11.7
version: 2.11.7
@@ -1014,10 +1014,10 @@ importers:
version: 2.1.1
react:
specifier: ^18.3.1
- version: 19.0.0
+ version: 18.3.1
react-dom:
specifier: ^18.3.1
- version: 19.0.0(react@19.0.0)
+ version: 18.3.1(react@18.3.1)
tippy.js:
specifier: ^6.3.7
version: 6.3.7
@@ -1055,6 +1055,9 @@ importers:
'@radix-ui/react-dropdown-menu':
specifier: ^2.1.7
version: 2.1.7(@types/react-dom@18.3.6(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-hover-card':
+ specifier: ^1.1.7
+ version: 1.1.7(@types/react-dom@18.3.6(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-label':
specifier: ^2.1.3
version: 2.1.3(@types/react-dom@18.3.6(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -2594,6 +2597,19 @@ packages:
'@types/react-dom':
optional: true
+ '@radix-ui/react-hover-card@1.1.7':
+ resolution: {integrity: sha512-HwM03kP8psrv21J1+9T/hhxi0f5rARVbqIZl9+IAq13l4j4fX+oGIuxisukZZmebO7J35w9gpoILvtG8bbph0w==}
+ 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-id@1.1.1':
resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==}
peerDependencies:
@@ -10287,6 +10303,23 @@ snapshots:
'@types/react': 18.3.20
'@types/react-dom': 18.3.6(@types/react@18.3.20)
+ '@radix-ui/react-hover-card@1.1.7(@types/react-dom@18.3.6(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.2
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.20)(react@18.3.1)
+ '@radix-ui/react-context': 1.1.2(@types/react@18.3.20)(react@18.3.1)
+ '@radix-ui/react-dismissable-layer': 1.1.6(@types/react-dom@18.3.6(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-popper': 1.2.3(@types/react-dom@18.3.6(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-portal': 1.1.5(@types/react-dom@18.3.6(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-presence': 1.1.3(@types/react-dom@18.3.6(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-primitive': 2.0.3(@types/react-dom@18.3.6(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-use-controllable-state': 1.1.1(@types/react@18.3.20)(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.20
+ '@types/react-dom': 18.3.6(@types/react@18.3.20)
+
'@radix-ui/react-id@1.1.1(@types/react@18.3.20)(react@18.3.1)':
dependencies:
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.20)(react@18.3.1)
@@ -10620,10 +10653,6 @@ snapshots:
dependencies:
react: 18.3.1
- '@remixicon/react@4.6.0(react@19.0.0)':
- dependencies:
- react: 19.0.0
-
'@rollup/pluginutils@5.1.4(rollup@4.39.0)':
dependencies:
'@types/estree': 1.0.7
@@ -11575,7 +11604,7 @@ snapshots:
prosemirror-transform: 1.10.3
prosemirror-view: 1.39.1
- '@tiptap/react@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+ '@tiptap/react@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@tiptap/core': 2.11.7(@tiptap/pm@2.11.7)
'@tiptap/extension-bubble-menu': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)
@@ -11583,9 +11612,9 @@ snapshots:
'@tiptap/pm': 2.11.7
'@types/use-sync-external-store': 0.0.6
fast-deep-equal: 3.1.3
- react: 19.0.0
- react-dom: 19.0.0(react@19.0.0)
- use-sync-external-store: 1.5.0(react@19.0.0)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ use-sync-external-store: 1.5.0(react@18.3.1)
'@tiptap/starter-kit@2.11.7':
dependencies:
@@ -17322,10 +17351,6 @@ snapshots:
dependencies:
react: 18.3.1
- use-sync-external-store@1.5.0(react@19.0.0):
- dependencies:
- react: 19.0.0
-
userhome@1.0.1: {}
util-deprecate@1.0.2: {}