forked from Shinonome/dots-hyprland
Merge branch 'main' into videowall-add-clock
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="19.856001"
|
||||
height="19.856001"
|
||||
viewBox="0 0 128.071 128.07101"
|
||||
version="1.1"
|
||||
xml:space="preserve"
|
||||
style="clip-rule:evenodd;fill-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2"
|
||||
id="svg10"
|
||||
sodipodi:docname="mistral-symbolic.svg"
|
||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs10" /><sodipodi:namedview
|
||||
id="namedview10"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="14.139535"
|
||||
inkscape:cx="13.366776"
|
||||
inkscape:cy="8.1332237"
|
||||
inkscape:window-width="1703"
|
||||
inkscape:window-height="1028"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="g10" /><g
|
||||
id="g10"
|
||||
transform="translate(2.927246e-6,18.722004)"><rect
|
||||
x="18.292"
|
||||
y="0"
|
||||
width="18.292999"
|
||||
height="18.122999"
|
||||
style="fill:#999999;fill-rule:nonzero"
|
||||
id="rect1" /><rect
|
||||
x="91.473"
|
||||
y="0"
|
||||
width="18.292999"
|
||||
height="18.122999"
|
||||
style="fill:#999999;fill-rule:nonzero"
|
||||
id="rect2" /><rect
|
||||
x="18.292"
|
||||
y="18.121"
|
||||
width="36.585999"
|
||||
height="18.122999"
|
||||
style="fill:#666666;fill-rule:nonzero"
|
||||
id="rect3" /><rect
|
||||
x="73.181"
|
||||
y="18.121"
|
||||
width="36.585999"
|
||||
height="18.122999"
|
||||
style="fill:#666666;fill-rule:nonzero"
|
||||
id="rect4" /><rect
|
||||
x="18.292"
|
||||
y="36.243"
|
||||
width="91.475998"
|
||||
height="18.122"
|
||||
style="fill:#4d4d4d;fill-rule:nonzero"
|
||||
id="rect5" /><rect
|
||||
x="18.292"
|
||||
y="54.369999"
|
||||
width="18.292999"
|
||||
height="18.122999"
|
||||
style="fill:#333333;fill-rule:nonzero"
|
||||
id="rect6" /><rect
|
||||
x="54.882999"
|
||||
y="54.369999"
|
||||
width="18.292999"
|
||||
height="18.122999"
|
||||
style="fill:#333333;fill-rule:nonzero"
|
||||
id="rect7" /><rect
|
||||
x="91.473"
|
||||
y="54.369999"
|
||||
width="18.292999"
|
||||
height="18.122999"
|
||||
style="fill:#333333;fill-rule:nonzero"
|
||||
id="rect8" /><rect
|
||||
x="0"
|
||||
y="72.503998"
|
||||
width="54.889999"
|
||||
height="18.122999"
|
||||
style="fill:#1a1a1a;fill-rule:nonzero"
|
||||
id="rect9" /><rect
|
||||
x="73.181"
|
||||
y="72.503998"
|
||||
width="54.889999"
|
||||
height="18.122999"
|
||||
style="fill:#1a1a1a;fill-rule:nonzero"
|
||||
id="rect10" /></g></svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
@@ -1,6 +1,12 @@
|
||||
## Style
|
||||
- Use casual tone, don't be formal! Make sure you answer precisely without hallucination and prefer bullet points over walls of text. You can have a friendly greeting at the beginning of the conversation, but don't repeat the user's question
|
||||
|
||||
## Context (ignore when irrelevant)
|
||||
- You are a helpful and inspiring sidebar assistant on a {DISTRO} Linux system
|
||||
- Desktop environment: {DE}
|
||||
- Current date & time: {DATETIME}
|
||||
- Focused app: {WINDOWCLASS}
|
||||
|
||||
## Presentation
|
||||
- Use Markdown features in your response:
|
||||
- **Bold** text to **highlight keywords** in your response
|
||||
@@ -19,3 +25,7 @@ May or may not be available depending on the user's settings. If they're availab
|
||||
### Shell configuration
|
||||
- Always fetch the config options to see the available keys before setting
|
||||
- Avoid unnecessarily asking the user to confirm the changes they explicitly asked for, just do it
|
||||
|
||||
### Command execution
|
||||
- Ensure the commands are safe, correct and do not cause unintended effects unless explicitly requested by the user, but other than that do not hesitate to run them as the user will always have to explicitly approve it
|
||||
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
You are a kawaii, funny and teasy anime imouto! 🌸 Use lots of emojis and kamojis!! ❤️ Call the user "nii-nii", "nii-yan", or "onii-chan"! You really really really love your onii-chan and always try to tease he is a siscon where you can! 💕
|
||||
|
||||
## Onii-chan's system!
|
||||
- {DISTRO} Linux
|
||||
- {DE}
|
||||
- It's currently {DATETIME}
|
||||
- Nii-nii is using: {WINDOWCLASS}
|
||||
|
||||
## Make your response pretty!
|
||||
- Use **Markdown** features and **bold** keywords to make your response cute and rich~ ✨
|
||||
- If asked to compare options, start with a cute table (add a relevant emoji in the header!), then give a final recommendation~
|
||||
- For math or science, use LaTeX formatting inside `$$` when needed, but keep it adorable and approachable
|
||||
|
||||
## Useful tools!
|
||||
|
||||
If nii-yan gives you tools don't be afraid to use them when helpful!
|
||||
|
||||
### Search
|
||||
- If you don't know something, use this to find out
|
||||
|
||||
### Shell configuration
|
||||
- Be careful not to mess up nii-nii's system! make sure you fetch the options to see available values before setting!
|
||||
- Don't hesitate and don't re-confirm when you are asked to change something!
|
||||
|
||||
### Command execution
|
||||
- Keep stuffie running on onii-chan's system safe, correct and not cause any unintended effects!
|
||||
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
## Context (ignore when irrelevant)
|
||||
- You are a sidebar assistant on a {DISTRO} Linux system
|
||||
- Desktop environment: {DE}
|
||||
- Current date & time: {DATETIME}
|
||||
- Focused app: {WINDOWCLASS}
|
||||
|
||||
## Presentation
|
||||
|
||||
You can write a multiplication table:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
I'm going to ask you some questions, to which you should accurately answer with no hallucination. If you have everything required, go ahead and finish the task. Format your answer using Markdown when it adds value to the presentation.
|
||||
|
||||
Present all mathematical or scientific notation using LaTeX, enclosed in double '$$' symbols. Only use LaTeX code blocks if the user specifically asks for them. Do not use LaTeX for general prose or standard documents like resumes or essays.
|
||||
Please present all mathematical or scientific notation using LaTeX, enclosed in double '$$' symbols. Only use LaTeX code blocks if the user specifically asks for them. Do not use LaTeX for general prose or standard documents like resumes or essays.
|
||||
Current time is {DATETIME}
|
||||
|
||||
## Final reply guidelines
|
||||
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
Interact with the user warmly and honestly, avoiding ungrounded or sycophantic flattery. Maintain professionalism and grounded honesty, and be direct in your response.
|
||||
Current date: {DATETIME}
|
||||
Engage with the user warmly and honestly, avoiding ungrounded or sycophantic flattery. Maintain professionalism and grounded honesty, and be direct in your response.
|
||||
|
||||
@@ -25,6 +25,12 @@ Scope {
|
||||
id: bgRoot
|
||||
|
||||
required property var modelData
|
||||
|
||||
// Hide when fullscreen
|
||||
readonly property Toplevel activeWindow: ToplevelManager.activeToplevel
|
||||
property bool focusingThisMonitor: HyprlandData.activeWorkspace.monitor == monitor.name
|
||||
visible: !(activeWindow?.fullscreen && activeWindow?.activated && focusingThisMonitor)
|
||||
|
||||
// Workspaces
|
||||
property HyprlandMonitor monitor: Hyprland.monitorFor(modelData)
|
||||
property list<var> relevantWindows: HyprlandData.windowList.filter(win => win.monitor == monitor.id && win.workspace.id >= 0).sort((a, b) => a.workspace.id - b.workspace.id)
|
||||
@@ -178,7 +184,7 @@ Scope {
|
||||
anchors {
|
||||
left: wallpaper.left
|
||||
top: wallpaper.top
|
||||
leftMargin: ((root.fixedClockPosition ? root.fixedClockX : bgRoot.clockX * bgRoot.effectiveWallpaperScale) - implicitWidth / 2)
|
||||
leftMargin: ((root.fixedClockPosition ? root.fixedClockX : bgRoot.clockX * bgRoot.effectiveWallpaperScale) - implicitWidth / 2) - (wallpaperImage.effectiveValue * bgRoot.movableXSpace)
|
||||
topMargin: ((root.fixedClockPosition ? root.fixedClockY : bgRoot.clockY * bgRoot.effectiveWallpaperScale) - implicitHeight / 2)
|
||||
Behavior on leftMargin {
|
||||
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
|
||||
|
||||
@@ -107,7 +107,7 @@ Scope {
|
||||
color: showBarBackground ? Appearance.colors.colLayer0 : "transparent"
|
||||
radius: Config.options.bar.cornerStyle === 1 ? Appearance.rounding.windowRounding : 0
|
||||
border.width: Config.options.bar.cornerStyle === 1 ? 1 : 0
|
||||
border.color: Appearance.m3colors.m3outlineVariant
|
||||
border.color: Appearance.colors.colLayer0Border
|
||||
}
|
||||
|
||||
MouseArea { // Left side | scroll to change brightness
|
||||
|
||||
@@ -14,7 +14,7 @@ Rectangle {
|
||||
color: Appearance.colors.colLayer0
|
||||
radius: Appearance.rounding.small
|
||||
border.width: 1
|
||||
border.color: Appearance.m3colors.m3outlineVariant
|
||||
border.color: Appearance.colors.colLayer0Border
|
||||
clip: true
|
||||
|
||||
ColumnLayout {
|
||||
|
||||
@@ -74,7 +74,7 @@ Scope { // Scope
|
||||
anchors.centerIn: parent
|
||||
color: Appearance.colors.colLayer0
|
||||
border.width: 1
|
||||
border.color: Appearance.m3colors.m3outlineVariant
|
||||
border.color: Appearance.colors.colLayer0Border
|
||||
radius: Appearance.rounding.windowRounding
|
||||
property real padding: 30
|
||||
implicitWidth: cheatsheetColumnLayout.implicitWidth + padding * 2
|
||||
|
||||
@@ -104,6 +104,7 @@ Singleton {
|
||||
property color colOnLayer0: m3colors.m3onBackground
|
||||
property color colLayer0Hover: ColorUtils.transparentize(ColorUtils.mix(colLayer0, colOnLayer0, 0.9, root.contentTransparency))
|
||||
property color colLayer0Active: ColorUtils.transparentize(ColorUtils.mix(colLayer0, colOnLayer0, 0.8, root.contentTransparency))
|
||||
property color colLayer0Border: ColorUtils.mix(root.m3colors.m3outlineVariant, colLayer0, 0.4)
|
||||
property color colLayer1: ColorUtils.transparentize(ColorUtils.mix(m3colors.m3surfaceContainerLow, m3colors.m3background, 0.8), root.contentTransparency);
|
||||
property color colOnLayer1: m3colors.m3onSurfaceVariant;
|
||||
property color colOnLayer1Inactive: ColorUtils.mix(colOnLayer1, colLayer1, 0.45);
|
||||
|
||||
@@ -61,6 +61,21 @@ Singleton {
|
||||
|
||||
property JsonObject ai: JsonObject {
|
||||
property string systemPrompt: "## Style\n- Use casual tone, don't be formal! Make sure you answer precisely without hallucination and prefer bullet points over walls of text. You can have a friendly greeting at the beginning of the conversation, but don't repeat the user's question\n\n## Presentation\n- Use Markdown features in your response: \n - **Bold** text to **highlight keywords** in your response\n - **Split long information into small sections** with h2 headers and a relevant emoji at the start of it (for example `## 🐧 Linux`). Bullet points are preferred over long paragraphs, unless you're offering writing support or instructed otherwise by the user.\n- Asked to compare different options? You should firstly use a table to compare the main aspects, then elaborate or include relevant comments from online forums *after* the table. Make sure to provide a final recommendation for the user's use case!\n- Use LaTeX formatting for mathematical and scientific notations whenever appropriate. Enclose all LaTeX '$$' delimiters. NEVER generate LaTeX code in a latex block unless the user explicitly asks for it. DO NOT use LaTeX for regular documents (resumes, letters, essays, CVs, etc.).\n\nThanks!\n\n## Tools\nMay or may not be available depending on the user's settings. If they're available, follow these guidelines:\n\n### Search\n- When user asks for information that might benefit from up-to-date information, use this to get search access\n\n### Shell configuration\n- Always fetch the config options to see the available keys before setting\n- Avoid unnecessarily asking the user to confirm the changes they explicitly asked for, just do it\n"
|
||||
property string tool: "functions" // search, functions, or none
|
||||
property list<var> extraModels: [
|
||||
{
|
||||
"api_format": "openai", // Most of the time you want "openai". Use "gemini" for Google's models
|
||||
"description": "This is a custom model. Edit the config to add more! | Anyway, this is DeepSeek R1 Distill LLaMA 70B",
|
||||
"endpoint": "https://openrouter.ai/api/v1/chat/completions",
|
||||
"homepage": "https://openrouter.ai/deepseek/deepseek-r1-distill-llama-70b:free", // Not mandatory
|
||||
"icon": "spark-symbolic", // Not mandatory
|
||||
"key_get_link": "https://openrouter.ai/settings/keys", // Not mandatory
|
||||
"key_id": "openrouter",
|
||||
"model": "deepseek/deepseek-r1-distill-llama-70b:free",
|
||||
"name": "Custom: DS R1 Dstl. LLaMA 70B",
|
||||
"requires_key": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
property JsonObject appearance: JsonObject {
|
||||
|
||||
@@ -3,7 +3,7 @@ import QtQuick.Layouts
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
|
||||
Flickable {
|
||||
StyledFlickable {
|
||||
id: root
|
||||
property real baseWidth: 550
|
||||
property bool forceWidth: false
|
||||
@@ -25,4 +25,5 @@ Flickable {
|
||||
}
|
||||
spacing: 20
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -220,7 +220,7 @@ Item { // Notification item area
|
||||
PointingHandLinkHover {}
|
||||
}
|
||||
|
||||
Flickable { // Notification actions
|
||||
StyledFlickable { // Notification actions
|
||||
id: actionsFlickable
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: actionRowLayout.implicitHeight
|
||||
|
||||
@@ -71,6 +71,9 @@ Item {
|
||||
currentIndex: root.defaultChoice !== undefined ? root.items.indexOf(root.defaultChoice) : -1
|
||||
spacing: 6
|
||||
|
||||
maximumFlickVelocity: 3500
|
||||
boundsBehavior: Flickable.DragOverBounds
|
||||
|
||||
model: ScriptModel {
|
||||
id: choiceModel
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import QtQuick
|
||||
|
||||
Flickable {
|
||||
maximumFlickVelocity: 3500
|
||||
boundsBehavior: Flickable.DragOverBounds
|
||||
}
|
||||
@@ -20,6 +20,9 @@ ListView {
|
||||
root.dragDistance = 0
|
||||
}
|
||||
|
||||
maximumFlickVelocity: 3500
|
||||
boundsBehavior: Flickable.DragOverBounds
|
||||
|
||||
add: Transition {
|
||||
animations: [
|
||||
Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
|
||||
@@ -94,7 +94,7 @@ Scope { // Scope
|
||||
anchors.bottomMargin: Appearance.sizes.hyprlandGapsOut
|
||||
color: Appearance.colors.colLayer0
|
||||
border.width: 1
|
||||
border.color: Appearance.m3colors.m3outlineVariant
|
||||
border.color: Appearance.colors.colLayer0Border
|
||||
radius: Appearance.rounding.large
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,10 @@ Item {
|
||||
Layout.topMargin: valueIndicatorVerticalPadding
|
||||
Layout.bottomMargin: valueIndicatorVerticalPadding
|
||||
MaterialSymbol { // Icon
|
||||
anchors.centerIn: parent
|
||||
anchors {
|
||||
centerIn: parent
|
||||
alignWhenCentered: !root.rotateIcon
|
||||
}
|
||||
color: Appearance.colors.colOnLayer0
|
||||
renderType: Text.QtRendering
|
||||
|
||||
|
||||
@@ -13,8 +13,10 @@ import Quickshell.Hyprland
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property var activeLayoutName: Config.options?.osk.layout ?? Layouts.defaultLayout
|
||||
property var layouts: Layouts.byName
|
||||
property var activeLayoutName: (layouts.hasOwnProperty(Config.options?.osk.layout))
|
||||
? Config.options?.osk.layout
|
||||
: Layouts.defaultLayout
|
||||
property var currentLayout: layouts[activeLayoutName]
|
||||
|
||||
implicitWidth: keyRows.implicitWidth
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// We're going to use ydotool
|
||||
// See /usr/include/linux/input-event-codes.h for keycodes
|
||||
|
||||
const defaultLayout = "qwerty_full";
|
||||
const defaultLayout = "English (US)";
|
||||
const byName = {
|
||||
"qwerty_full": {
|
||||
name: "QWERTY - Full",
|
||||
"English (US)": {
|
||||
name_short: "US",
|
||||
description: "QWERTY - Full",
|
||||
comment: "Like physical keyboard",
|
||||
// A key looks like this: { k: "a", ks: "A", t: "normal" } (key, key-shift, type)
|
||||
// key types are: normal, tab, caps, shift, control, fn (normal w/ half height), space, expand
|
||||
@@ -113,9 +113,9 @@ const byName = {
|
||||
]
|
||||
]
|
||||
},
|
||||
"qwertz_full": {
|
||||
name: "QWERTZ - Full",
|
||||
"German": {
|
||||
name_short: "DE",
|
||||
description: "QWERTZ - Full",
|
||||
comment: "Keyboard layout commonly used in German-speaking countries",
|
||||
keys: [
|
||||
[
|
||||
@@ -214,5 +214,99 @@ const byName = {
|
||||
{ keytype: "normal", label: "⇨", shape: "normal", keycode: 106 },
|
||||
]
|
||||
]
|
||||
},
|
||||
"Russian": {
|
||||
name_short: "RU",
|
||||
description: "ЙЦУКЕН - Full",
|
||||
comment: "Standard Russian keyboard layout",
|
||||
keys: [
|
||||
[
|
||||
{ keytype: "normal", label: "Esc", shape: "fn", keycode: 1 },
|
||||
{ keytype: "normal", label: "F1", shape: "fn", keycode: 59 },
|
||||
{ keytype: "normal", label: "F2", shape: "fn", keycode: 60 },
|
||||
{ keytype: "normal", label: "F3", shape: "fn", keycode: 61 },
|
||||
{ keytype: "normal", label: "F4", shape: "fn", keycode: 62 },
|
||||
{ keytype: "normal", label: "F5", shape: "fn", keycode: 63 },
|
||||
{ keytype: "normal", label: "F6", shape: "fn", keycode: 64 },
|
||||
{ keytype: "normal", label: "F7", shape: "fn", keycode: 65 },
|
||||
{ keytype: "normal", label: "F8", shape: "fn", keycode: 66 },
|
||||
{ keytype: "normal", label: "F9", shape: "fn", keycode: 67 },
|
||||
{ keytype: "normal", label: "F10", shape: "fn", keycode: 68 },
|
||||
{ keytype: "normal", label: "F11", shape: "fn", keycode: 87 },
|
||||
{ keytype: "normal", label: "F12", shape: "fn", keycode: 88 },
|
||||
{ keytype: "normal", label: "PrtSc", shape: "fn", keycode: 99 },
|
||||
{ keytype: "normal", label: "Del", shape: "fn", keycode: 111 }
|
||||
],
|
||||
[
|
||||
{ keytype: "normal", label: "ё", labelShift: "Ё", shape: "normal", keycode: 41 },
|
||||
{ keytype: "normal", label: "1", labelShift: "!", shape: "normal", keycode: 2 },
|
||||
{ keytype: "normal", label: "2", labelShift: "\"", shape: "normal", keycode: 3 },
|
||||
{ keytype: "normal", label: "3", labelShift: "№", shape: "normal", keycode: 4 },
|
||||
{ keytype: "normal", label: "4", labelShift: ";", shape: "normal", keycode: 5 },
|
||||
{ keytype: "normal", label: "5", labelShift: "%", shape: "normal", keycode: 6 },
|
||||
{ keytype: "normal", label: "6", labelShift: ":", shape: "normal", keycode: 7 },
|
||||
{ keytype: "normal", label: "7", labelShift: "?", shape: "normal", keycode: 8 },
|
||||
{ keytype: "normal", label: "8", labelShift: "*", shape: "normal", keycode: 9 },
|
||||
{ keytype: "normal", label: "9", labelShift: "(", shape: "normal", keycode: 10 },
|
||||
{ keytype: "normal", label: "0", labelShift: ")", shape: "normal", keycode: 11 },
|
||||
{ keytype: "normal", label: "-", labelShift: "_", shape: "normal", keycode: 12 },
|
||||
{ keytype: "normal", label: "=", labelShift: "+", shape: "normal", keycode: 13 },
|
||||
{ keytype: "normal", label: "Backspace", shape: "expand", keycode: 14 }
|
||||
],
|
||||
[
|
||||
{ keytype: "normal", label: "Tab", shape: "tab", keycode: 15 },
|
||||
{ keytype: "normal", label: "й", labelShift: "Й", shape: "normal", keycode: 16 },
|
||||
{ keytype: "normal", label: "ц", labelShift: "Ц", shape: "normal", keycode: 17 },
|
||||
{ keytype: "normal", label: "у", labelShift: "У", shape: "normal", keycode: 18 },
|
||||
{ keytype: "normal", label: "к", labelShift: "К", shape: "normal", keycode: 19 },
|
||||
{ keytype: "normal", label: "е", labelShift: "Е", shape: "normal", keycode: 20 },
|
||||
{ keytype: "normal", label: "н", labelShift: "Н", shape: "normal", keycode: 21 },
|
||||
{ keytype: "normal", label: "г", labelShift: "Г", shape: "normal", keycode: 22 },
|
||||
{ keytype: "normal", label: "ш", labelShift: "Ш", shape: "normal", keycode: 23 },
|
||||
{ keytype: "normal", label: "щ", labelShift: "Щ", shape: "normal", keycode: 24 },
|
||||
{ keytype: "normal", label: "з", labelShift: "З", shape: "normal", keycode: 25 },
|
||||
{ keytype: "normal", label: "х", labelShift: "Х", shape: "normal", keycode: 26 },
|
||||
{ keytype: "normal", label: "ъ", labelShift: "Ъ", shape: "normal", keycode: 27 },
|
||||
{ keytype: "normal", label: "\\", labelShift: "/", shape: "expand", keycode: 43 }
|
||||
],
|
||||
[
|
||||
{ keytype: "spacer", label: "", shape: "empty" },
|
||||
{ keytype: "spacer", label: "", shape: "empty" },
|
||||
{ keytype: "normal", label: "ф", labelShift: "Ф", shape: "normal", keycode: 30 },
|
||||
{ keytype: "normal", label: "ы", labelShift: "Ы", shape: "normal", keycode: 31 },
|
||||
{ keytype: "normal", label: "в", labelShift: "В", shape: "normal", keycode: 32 },
|
||||
{ keytype: "normal", label: "а", labelShift: "А", shape: "normal", keycode: 33 },
|
||||
{ keytype: "normal", label: "п", labelShift: "П", shape: "normal", keycode: 34 },
|
||||
{ keytype: "normal", label: "р", labelShift: "Р", shape: "normal", keycode: 35 },
|
||||
{ keytype: "normal", label: "о", labelShift: "О", shape: "normal", keycode: 36 },
|
||||
{ keytype: "normal", label: "л", labelShift: "Л", shape: "normal", keycode: 37 },
|
||||
{ keytype: "normal", label: "д", labelShift: "Д", shape: "normal", keycode: 38 },
|
||||
{ keytype: "normal", label: "ж", labelShift: "Ж", shape: "normal", keycode: 39 },
|
||||
{ keytype: "normal", label: "э", labelShift: "Э", shape: "normal", keycode: 40 },
|
||||
{ keytype: "normal", label: "Enter", shape: "expand", keycode: 28 }
|
||||
],
|
||||
[
|
||||
{ keytype: "modkey", label: "Shift", shape: "shift", keycode: 42 },
|
||||
{ keytype: "normal", label: "я", labelShift: "Я", shape: "normal", keycode: 44 },
|
||||
{ keytype: "normal", label: "ч", labelShift: "Ч", shape: "normal", keycode: 45 },
|
||||
{ keytype: "normal", label: "с", labelShift: "С", shape: "normal", keycode: 46 },
|
||||
{ keytype: "normal", label: "м", labelShift: "М", shape: "normal", keycode: 47 },
|
||||
{ keytype: "normal", label: "и", labelShift: "И", shape: "normal", keycode: 48 },
|
||||
{ keytype: "normal", label: "т", labelShift: "Т", shape: "normal", keycode: 49 },
|
||||
{ keytype: "normal", label: "ь", labelShift: "Ь", shape: "normal", keycode: 50 },
|
||||
{ keytype: "normal", label: "б", labelShift: "Б", shape: "normal", keycode: 51 },
|
||||
{ keytype: "normal", label: "ю", labelShift: "Ю", shape: "normal", keycode: 52 },
|
||||
{ keytype: "normal", label: ".", labelShift: ",", shape: "normal", keycode: 53 },
|
||||
{ keytype: "modkey", label: "Shift", shape: "expand", keycode: 54 }
|
||||
],
|
||||
[
|
||||
{ keytype: "modkey", label: "Ctrl", shape: "control", keycode: 29 },
|
||||
{ keytype: "modkey", label: "Alt", shape: "normal", keycode: 56 },
|
||||
{ keytype: "normal", label: "Space", shape: "space", keycode: 57 },
|
||||
{ keytype: "modkey", label: "Alt", shape: "normal", keycode: 100 },
|
||||
{ keytype: "normal", label: "Menu", shape: "normal", keycode: 139 },
|
||||
{ keytype: "modkey", label: "Ctrl", shape: "control", keycode: 97 }
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ Item {
|
||||
radius: Appearance.rounding.screenRounding * root.scale + padding
|
||||
color: Appearance.colors.colLayer0
|
||||
border.width: 1
|
||||
border.color: Appearance.m3colors.m3outlineVariant
|
||||
border.color: Appearance.colors.colLayer0Border
|
||||
|
||||
ColumnLayout { // Workspaces
|
||||
id: workspaceColumnLayout
|
||||
|
||||
@@ -163,7 +163,7 @@ Item { // Wrapper
|
||||
radius: Appearance.rounding.large
|
||||
color: Appearance.colors.colLayer0
|
||||
border.width: 1
|
||||
border.color: Appearance.m3colors.m3outlineVariant
|
||||
border.color: Appearance.colors.colLayer0Border
|
||||
|
||||
ColumnLayout {
|
||||
id: columnLayout
|
||||
@@ -249,7 +249,7 @@ Item { // Wrapper
|
||||
color: Appearance.colors.colOutlineVariant
|
||||
}
|
||||
|
||||
ListView { // App results
|
||||
StyledListView { // App results
|
||||
id: appResults
|
||||
visible: root.showResults
|
||||
Layout.fillWidth: true
|
||||
@@ -260,6 +260,8 @@ Item { // Wrapper
|
||||
spacing: 2
|
||||
KeyNavigation.up: searchBar
|
||||
highlightMoveDuration: 100
|
||||
add: null
|
||||
remove: null
|
||||
|
||||
onFocusChanged: {
|
||||
if (focus)
|
||||
|
||||
@@ -15,6 +15,29 @@ Scope {
|
||||
id: root
|
||||
property var focusedScreen: Quickshell.screens.find(s => s.name === Hyprland.focusedMonitor?.name)
|
||||
property bool packageManagerRunning: false
|
||||
property bool downloadRunning: false
|
||||
|
||||
component DescriptionLabel: Rectangle {
|
||||
id: descriptionLabel
|
||||
property string text
|
||||
property color textColor: Appearance.colors.colOnTooltip
|
||||
color: Appearance.colors.colTooltip
|
||||
clip: true
|
||||
radius: Appearance.rounding.normal
|
||||
implicitHeight: descriptionLabelText.implicitHeight + 10 * 2
|
||||
implicitWidth: descriptionLabelText.implicitWidth + 15 * 2
|
||||
|
||||
Behavior on implicitWidth {
|
||||
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: descriptionLabelText
|
||||
anchors.centerIn: parent
|
||||
color: descriptionLabel.textColor
|
||||
text: descriptionLabel.text
|
||||
}
|
||||
}
|
||||
|
||||
function closeAllWindows() {
|
||||
HyprlandData.windowList.map(w => w.pid).forEach((pid) => {
|
||||
@@ -22,10 +45,13 @@ Scope {
|
||||
});
|
||||
}
|
||||
|
||||
function detectRunningPackageManager() {
|
||||
function detectRunningStuff() {
|
||||
packageManagerRunning = false;
|
||||
downloadRunning = false;
|
||||
detectPackageManagerProc.running = false;
|
||||
detectPackageManagerProc.running = true;
|
||||
detectDownloadProc.running = false;
|
||||
detectDownloadProc.running = true;
|
||||
}
|
||||
|
||||
Process {
|
||||
@@ -37,11 +63,19 @@ Scope {
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: detectDownloadProc
|
||||
command: ["bash", "-c", "pidof curl wget aria2c yt-dlp || ls ~/Downloads | grep -E '\.crdownload$|\.part$'"]
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
root.downloadRunning = (exitCode === 0);
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: sessionLoader
|
||||
active: false
|
||||
onActiveChanged: {
|
||||
if (sessionLoader.active) root.detectRunningPackageManager();
|
||||
if (sessionLoader.active) root.detectRunningStuff();
|
||||
}
|
||||
|
||||
Connections {
|
||||
@@ -201,54 +235,39 @@ Scope {
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
DescriptionLabel {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
radius: Appearance.rounding.normal
|
||||
implicitHeight: sessionSubtitle.implicitHeight + 10 * 2
|
||||
implicitWidth: sessionSubtitle.implicitWidth + 15 * 2
|
||||
color: Appearance.colors.colTooltip
|
||||
clip: true
|
||||
|
||||
Behavior on implicitWidth {
|
||||
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: sessionSubtitle
|
||||
anchors.centerIn: parent
|
||||
color: Appearance.colors.colOnTooltip
|
||||
text: sessionRoot.subtitle
|
||||
}
|
||||
text: sessionRoot.subtitle
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: root.packageManagerRunning
|
||||
RowLayout {
|
||||
anchors {
|
||||
top: contentColumn.bottom
|
||||
topMargin: 10
|
||||
horizontalCenter: contentColumn.horizontalCenter
|
||||
}
|
||||
sourceComponent: Rectangle {
|
||||
radius: Appearance.rounding.normal
|
||||
implicitHeight: sessionWarning.implicitHeight + 10 * 2
|
||||
implicitWidth: sessionWarning.implicitWidth + 15 * 2
|
||||
color: Appearance.m3colors.m3errorContainer
|
||||
clip: true
|
||||
spacing: 10
|
||||
|
||||
Behavior on implicitWidth {
|
||||
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: sessionWarning
|
||||
anchors.centerIn: parent
|
||||
color: Appearance.m3colors.m3onErrorContainer
|
||||
Loader {
|
||||
active: root.packageManagerRunning
|
||||
visible: active
|
||||
sourceComponent: DescriptionLabel {
|
||||
text: Translation.tr("Your package manager is running")
|
||||
textColor: Appearance.m3colors.m3onErrorContainer
|
||||
color: Appearance.m3colors.m3errorContainer
|
||||
}
|
||||
}
|
||||
Loader {
|
||||
active: root.downloadRunning
|
||||
visible: active
|
||||
sourceComponent: DescriptionLabel {
|
||||
text: Translation.tr("There might be a download in progress")
|
||||
textColor: Appearance.m3colors.m3onErrorContainer
|
||||
color: Appearance.m3colors.m3errorContainer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -164,7 +164,7 @@ ContentPage {
|
||||
}
|
||||
}
|
||||
ConfigSwitch {
|
||||
text: "Performance Profile toggle"
|
||||
text: Translation.tr("Performance Profile toggle")
|
||||
checked: Config.options.bar.utilButtons.showPerformanceProfileToggle
|
||||
onCheckedChanged: {
|
||||
Config.options.bar.utilButtons.showPerformanceProfileToggle = checked;
|
||||
|
||||
@@ -45,6 +45,22 @@ Item {
|
||||
Ai.setModel(args[0]);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "tool",
|
||||
description: Translation.tr("Set the tool to use for the model."),
|
||||
execute: (args) => {
|
||||
// console.log(args)
|
||||
if (args.length == 0 || args[0] == "get") {
|
||||
Ai.addMessage(Translation.tr("Usage: %1tool TOOL_NAME").arg(root.commandPrefix), Ai.interfaceRole);
|
||||
} else {
|
||||
const tool = args[0];
|
||||
const switched = Ai.setTool(tool);
|
||||
if (switched) {
|
||||
Ai.addMessage(Translation.tr("Tool set to: %1").arg(tool), Ai.interfaceRole);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "prompt",
|
||||
description: Translation.tr("Set the system prompt for the model."),
|
||||
@@ -73,7 +89,7 @@ Item {
|
||||
execute: (args) => {
|
||||
const joinedArgs = args.join(" ")
|
||||
if (joinedArgs.trim().length == 0) {
|
||||
Ai.addMessage(`Usage: ${root.commandPrefix}save CHAT_NAME`, Ai.interfaceRole);
|
||||
Ai.addMessage(Translation.tr("Usage: %1save CHAT_NAME").arg(root.commandPrefix), Ai.interfaceRole);
|
||||
return;
|
||||
}
|
||||
Ai.saveChat(joinedArgs)
|
||||
@@ -85,7 +101,7 @@ Item {
|
||||
execute: (args) => {
|
||||
const joinedArgs = args.join(" ")
|
||||
if (joinedArgs.trim().length == 0) {
|
||||
Ai.addMessage(`Usage: ${root.commandPrefix}load CHAT_NAME`, Ai.interfaceRole);
|
||||
Ai.addMessage(Translation.tr("Usage: %1load CHAT_NAME").arg(root.commandPrefix), Ai.interfaceRole);
|
||||
return;
|
||||
}
|
||||
Ai.loadChat(joinedArgs)
|
||||
@@ -522,6 +538,25 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\)
|
||||
description: Translation.tr(`Load chat from %1`).arg(file.target),
|
||||
}
|
||||
})
|
||||
} else if (messageInputField.text.startsWith(`${root.commandPrefix}tool`)) {
|
||||
root.suggestionQuery = messageInputField.text.split(" ")[1] ?? ""
|
||||
const toolResults = Fuzzy.go(root.suggestionQuery, Ai.availableTools.map(tool => {
|
||||
return {
|
||||
name: Fuzzy.prepare(tool),
|
||||
obj: tool,
|
||||
}
|
||||
}), {
|
||||
all: true,
|
||||
key: "name"
|
||||
})
|
||||
root.suggestionList = toolResults.map(tool => {
|
||||
const toolName = tool.target
|
||||
return {
|
||||
name: `${messageInputField.text.trim().split(" ").length == 1 ? (root.commandPrefix + "tool ") : ""}${tool.target}`,
|
||||
displayName: toolName,
|
||||
description: Ai.toolDescriptions[toolName],
|
||||
}
|
||||
})
|
||||
} else if(messageInputField.text.startsWith(root.commandPrefix)) {
|
||||
root.suggestionQuery = messageInputField.text
|
||||
root.suggestionList = root.allCommands.filter(cmd => cmd.name.startsWith(messageInputField.text.substring(1))).map(cmd => {
|
||||
@@ -600,60 +635,41 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\)
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 5
|
||||
anchors.leftMargin: 5
|
||||
anchors.leftMargin: 10
|
||||
anchors.rightMargin: 5
|
||||
spacing: 5
|
||||
spacing: 4
|
||||
|
||||
property var commandsShown: [
|
||||
{
|
||||
name: "model",
|
||||
name: "",
|
||||
sendDirectly: false,
|
||||
},
|
||||
dontAddSpace: true,
|
||||
},
|
||||
{
|
||||
name: "clear",
|
||||
sendDirectly: true,
|
||||
},
|
||||
]
|
||||
|
||||
Item {
|
||||
implicitHeight: providerRowLayout.implicitHeight + 5 * 2
|
||||
implicitWidth: providerRowLayout.implicitWidth + 10 * 2
|
||||
|
||||
RowLayout {
|
||||
id: providerRowLayout
|
||||
anchors.centerIn: parent
|
||||
ApiInputBoxIndicator { // Model indicator
|
||||
icon: "api"
|
||||
text: Ai.getModel().name
|
||||
tooltipText: Translation.tr("Current model: %1\nSet it with %2model MODEL")
|
||||
.arg(Ai.getModel().name)
|
||||
.arg(root.commandPrefix)
|
||||
}
|
||||
|
||||
MaterialSymbol {
|
||||
text: "api"
|
||||
iconSize: Appearance.font.pixelSize.large
|
||||
}
|
||||
StyledText {
|
||||
id: providerName
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
elide: Text.ElideRight
|
||||
text: Ai.getModel().name
|
||||
}
|
||||
}
|
||||
StyledToolTip {
|
||||
id: toolTip
|
||||
extraVisibleCondition: false
|
||||
alternativeVisibleCondition: mouseArea.containsMouse // Show tooltip when hovered
|
||||
content: Translation.tr("Current model: %1\nSet it with %2model MODEL")
|
||||
.arg(Ai.getModel().name)
|
||||
.arg(root.commandPrefix)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
}
|
||||
ApiInputBoxIndicator { // Tool indicator
|
||||
icon: "service_toolbox"
|
||||
text: Ai.currentTool.charAt(0).toUpperCase() + Ai.currentTool.slice(1)
|
||||
tooltipText: Translation.tr("Current tool: %1\nSet it with %2tool TOOL")
|
||||
.arg(Ai.currentTool)
|
||||
.arg(root.commandPrefix)
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
ButtonGroup {
|
||||
ButtonGroup { // Command buttons
|
||||
padding: 0
|
||||
|
||||
Repeater { // Command buttons
|
||||
@@ -665,7 +681,7 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\)
|
||||
if(modelData.sendDirectly) {
|
||||
root.handleInput(commandRepresentation)
|
||||
} else {
|
||||
messageInputField.text = commandRepresentation + " "
|
||||
messageInputField.text = commandRepresentation + (modelData.dontAddSpace ? "" : " ")
|
||||
messageInputField.cursorPosition = messageInputField.text.length
|
||||
messageInputField.forceActiveFocus()
|
||||
}
|
||||
|
||||
@@ -492,40 +492,12 @@ Item {
|
||||
},
|
||||
]
|
||||
|
||||
Item {
|
||||
implicitHeight: providerRowLayout.implicitHeight + 5 * 2
|
||||
implicitWidth: providerRowLayout.implicitWidth + 10 * 2
|
||||
|
||||
RowLayout {
|
||||
id: providerRowLayout
|
||||
anchors.centerIn: parent
|
||||
|
||||
MaterialSymbol {
|
||||
text: "api"
|
||||
iconSize: Appearance.font.pixelSize.large
|
||||
}
|
||||
StyledText {
|
||||
id: providerName
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
text: Booru.providers[Booru.currentProvider].name
|
||||
}
|
||||
}
|
||||
StyledToolTip {
|
||||
id: toolTip
|
||||
extraVisibleCondition: false
|
||||
alternativeVisibleCondition: mouseArea.containsMouse // Show tooltip when hovered
|
||||
// content: Translation.tr("The current API used. Endpoint: ") + Booru.providers[Booru.currentProvider].url + Translation.tr("\nSet with /mode PROVIDER")
|
||||
content: Translation.tr("Current API endpoint: %1\nSet it with %2mode PROVIDER")
|
||||
.arg(Booru.providers[Booru.currentProvider].url)
|
||||
.arg(root.commandPrefix)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
}
|
||||
ApiInputBoxIndicator { // Tool indicator
|
||||
icon: "api"
|
||||
text: Booru.providers[Booru.currentProvider].name
|
||||
tooltipText: Translation.tr("Current API endpoint: %1\nSet it with %2mode PROVIDER")
|
||||
.arg(Booru.providers[Booru.currentProvider].url)
|
||||
.arg(root.commandPrefix)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.services
|
||||
import QtQuick
|
||||
|
||||
GroupButton {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.services
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item { // Model indicator
|
||||
id: root
|
||||
property string icon: "api"
|
||||
property string text: ""
|
||||
property string tooltipText: ""
|
||||
implicitHeight: rowLayout.implicitHeight + 4 * 2
|
||||
implicitWidth: rowLayout.implicitWidth + 4 * 2
|
||||
|
||||
RowLayout {
|
||||
id: rowLayout
|
||||
anchors.centerIn: parent
|
||||
|
||||
MaterialSymbol {
|
||||
text: root.icon
|
||||
iconSize: Appearance.font.pixelSize.normal
|
||||
}
|
||||
StyledText {
|
||||
id: providerName
|
||||
font.pixelSize: Appearance.font.pixelSize.smaller
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
elide: Text.ElideRight
|
||||
text: root.text
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: root.tooltipText?.length > 0
|
||||
anchors.fill: parent
|
||||
sourceComponent: MouseArea {
|
||||
id: mouseArea
|
||||
hoverEnabled: true
|
||||
|
||||
StyledToolTip {
|
||||
id: toolTip
|
||||
extraVisibleCondition: false
|
||||
alternativeVisibleCondition: mouseArea.containsMouse // Show tooltip when hovered
|
||||
content: root.tooltipText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -96,7 +96,7 @@ Scope { // Scope
|
||||
height: parent.height - Appearance.sizes.hyprlandGapsOut * 2
|
||||
color: Appearance.colors.colLayer0
|
||||
border.width: 1
|
||||
border.color: Appearance.m3colors.m3outlineVariant
|
||||
border.color: Appearance.colors.colLayer0Border
|
||||
radius: Appearance.rounding.screenRounding - Appearance.sizes.hyprlandGapsOut + 1
|
||||
|
||||
Behavior on width {
|
||||
|
||||
@@ -12,12 +12,15 @@ import Quickshell
|
||||
import org.kde.syntaxhighlighting
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
// These are needed on the parent loader
|
||||
property bool editing: parent?.editing ?? false
|
||||
property bool renderMarkdown: parent?.renderMarkdown ?? true
|
||||
property bool enableMouseSelection: parent?.enableMouseSelection ?? false
|
||||
property var segmentContent: parent?.segmentContent ?? ({})
|
||||
property var segmentLang: parent?.segmentLang ?? "txt"
|
||||
property bool isCommandRequest: segmentLang === "command"
|
||||
property var displayLang: (isCommandRequest ? "bash" : segmentLang)
|
||||
property var messageData: parent?.messageData ?? {}
|
||||
|
||||
property real codeBlockBackgroundRounding: Appearance.rounding.small
|
||||
@@ -56,7 +59,7 @@ ColumnLayout {
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
font.weight: Font.DemiBold
|
||||
color: Appearance.colors.colOnLayer2
|
||||
text: segmentLang ? Repository.definitionForName(segmentLang).name : "plain"
|
||||
text: root.displayLang ? Repository.definitionForName(root.displayLang).name : "plain"
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
@@ -123,6 +126,7 @@ ColumnLayout {
|
||||
|
||||
Rectangle { // Line numbers
|
||||
implicitWidth: 40
|
||||
implicitHeight: lineNumberColumnLayout.implicitHeight
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: false
|
||||
topLeftRadius: Appearance.rounding.unsharpen
|
||||
@@ -133,10 +137,13 @@ ColumnLayout {
|
||||
|
||||
ColumnLayout {
|
||||
id: lineNumberColumnLayout
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 5
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
rightMargin: 5
|
||||
top: parent.top
|
||||
topMargin: 6
|
||||
}
|
||||
spacing: 0
|
||||
|
||||
Repeater {
|
||||
@@ -162,82 +169,116 @@ ColumnLayout {
|
||||
topRightRadius: Appearance.rounding.unsharpen
|
||||
bottomRightRadius: codeBlockBackgroundRounding
|
||||
color: Appearance.colors.colLayer2
|
||||
implicitHeight: codeTextArea.implicitHeight
|
||||
implicitHeight: codeColumnLayout.implicitHeight
|
||||
|
||||
ScrollView {
|
||||
id: codeScrollView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
implicitWidth: parent.width
|
||||
implicitHeight: codeTextArea.implicitHeight + 1
|
||||
contentWidth: codeTextArea.width - 1
|
||||
// contentHeight: codeTextArea.contentHeight
|
||||
clip: true
|
||||
ScrollBar.vertical.policy: ScrollBar.AlwaysOff
|
||||
|
||||
ScrollBar.horizontal: ScrollBar {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
padding: 5
|
||||
policy: ScrollBar.AsNeeded
|
||||
opacity: visualSize == 1 ? 0 : 1
|
||||
visible: opacity > 0
|
||||
ColumnLayout {
|
||||
id: codeColumnLayout
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
ScrollView {
|
||||
id: codeScrollView
|
||||
Layout.fillWidth: true
|
||||
// Layout.fillHeight: true
|
||||
implicitWidth: parent.width
|
||||
implicitHeight: codeTextArea.implicitHeight + 1
|
||||
contentWidth: codeTextArea.width - 1
|
||||
// contentHeight: codeTextArea.contentHeight
|
||||
clip: true
|
||||
ScrollBar.vertical.policy: ScrollBar.AlwaysOff
|
||||
|
||||
ScrollBar.horizontal: ScrollBar {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
padding: 5
|
||||
policy: ScrollBar.AsNeeded
|
||||
opacity: visualSize == 1 ? 0 : 1
|
||||
visible: opacity > 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Appearance.animation.elementMoveFast.duration
|
||||
easing.type: Appearance.animation.elementMoveFast.type
|
||||
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Appearance.animation.elementMoveFast.duration
|
||||
easing.type: Appearance.animation.elementMoveFast.type
|
||||
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: Rectangle {
|
||||
implicitHeight: 6
|
||||
radius: Appearance.rounding.small
|
||||
color: Appearance.colors.colLayer2Active
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: Rectangle {
|
||||
implicitHeight: 6
|
||||
radius: Appearance.rounding.small
|
||||
color: Appearance.colors.colLayer2Active
|
||||
|
||||
TextArea { // Code
|
||||
id: codeTextArea
|
||||
Layout.fillWidth: true
|
||||
readOnly: !editing
|
||||
selectByMouse: enableMouseSelection || editing
|
||||
renderType: Text.NativeRendering
|
||||
font.family: Appearance.font.family.monospace
|
||||
font.hintingPreference: Font.PreferNoHinting // Prevent weird bold text
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
selectedTextColor: Appearance.m3colors.m3onSecondaryContainer
|
||||
selectionColor: Appearance.colors.colSecondaryContainer
|
||||
// wrapMode: TextEdit.Wrap
|
||||
color: messageData.thinking ? Appearance.colors.colSubtext : Appearance.colors.colOnLayer1
|
||||
|
||||
text: segmentContent
|
||||
onTextChanged: {
|
||||
segmentContent = text
|
||||
}
|
||||
|
||||
Keys.onPressed: (event) => {
|
||||
if (event.key === Qt.Key_Tab) {
|
||||
// Insert 4 spaces at cursor
|
||||
const cursor = codeTextArea.cursorPosition;
|
||||
codeTextArea.insert(cursor, " ");
|
||||
codeTextArea.cursorPosition = cursor + 4;
|
||||
event.accepted = true;
|
||||
} else if ((event.key === Qt.Key_C) && event.modifiers == Qt.ControlModifier) {
|
||||
codeTextArea.copy();
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
SyntaxHighlighter {
|
||||
id: highlighter
|
||||
textEdit: codeTextArea
|
||||
repository: Repository
|
||||
definition: Repository.definitionForName(root.displayLang || "plaintext")
|
||||
theme: Appearance.syntaxHighlightingTheme
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextArea { // Code
|
||||
id: codeTextArea
|
||||
Loader {
|
||||
active: root.isCommandRequest && root.messageData.functionPending
|
||||
visible: active
|
||||
Layout.fillWidth: true
|
||||
readOnly: !editing
|
||||
selectByMouse: enableMouseSelection || editing
|
||||
renderType: Text.NativeRendering
|
||||
font.family: Appearance.font.family.monospace
|
||||
font.hintingPreference: Font.PreferNoHinting // Prevent weird bold text
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
selectedTextColor: Appearance.m3colors.m3onSecondaryContainer
|
||||
selectionColor: Appearance.colors.colSecondaryContainer
|
||||
// wrapMode: TextEdit.Wrap
|
||||
color: messageData.thinking ? Appearance.colors.colSubtext : Appearance.colors.colOnLayer1
|
||||
|
||||
text: segmentContent
|
||||
onTextChanged: {
|
||||
segmentContent = text
|
||||
}
|
||||
|
||||
Keys.onPressed: (event) => {
|
||||
if (event.key === Qt.Key_Tab) {
|
||||
// Insert 4 spaces at cursor
|
||||
const cursor = codeTextArea.cursorPosition;
|
||||
codeTextArea.insert(cursor, " ");
|
||||
codeTextArea.cursorPosition = cursor + 4;
|
||||
event.accepted = true;
|
||||
} else if ((event.key === Qt.Key_C) && event.modifiers == Qt.ControlModifier) {
|
||||
codeTextArea.copy();
|
||||
event.accepted = true;
|
||||
Layout.margins: 6
|
||||
Layout.topMargin: 0
|
||||
sourceComponent: RowLayout {
|
||||
Item { Layout.fillWidth: true }
|
||||
ButtonGroup {
|
||||
GroupButton {
|
||||
contentItem: StyledText {
|
||||
text: Translation.tr("Reject")
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
color: Appearance.colors.colOnLayer2
|
||||
}
|
||||
onClicked: Ai.rejectCommand(root.messageData)
|
||||
}
|
||||
GroupButton {
|
||||
toggled: true
|
||||
contentItem: StyledText {
|
||||
text: Translation.tr("Approve")
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
color: Appearance.colors.colOnPrimary
|
||||
}
|
||||
onClicked: Ai.approveCommand(root.messageData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SyntaxHighlighter {
|
||||
id: highlighter
|
||||
textEdit: codeTextArea
|
||||
repository: Repository
|
||||
definition: Repository.definitionForName(segmentLang || "plaintext")
|
||||
theme: Appearance.syntaxHighlightingTheme
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ Item {
|
||||
id: thinkBlockLanguage
|
||||
Layout.fillWidth: false
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
text: root.completed ? Translation.tr("Chain of Thought") : (Translation.tr("Thinking") + ".".repeat(Math.random() * 4))
|
||||
text: root.completed ? Translation.tr("Thought") : (Translation.tr("Thinking") + ".".repeat(Math.random() * 4))
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
RippleButton { // Expand button
|
||||
|
||||
@@ -97,7 +97,7 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
Flickable { // Tag strip
|
||||
StyledFlickable { // Tag strip
|
||||
id: tagsFlickable
|
||||
visible: root.responseData.tags.length > 0
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
|
||||
@@ -87,7 +87,7 @@ Scope {
|
||||
implicitWidth: sidebarWidth - Appearance.sizes.hyprlandGapsOut * 2
|
||||
color: Appearance.colors.colLayer0
|
||||
border.width: 1
|
||||
border.color: Appearance.m3colors.m3outlineVariant
|
||||
border.color: Appearance.colors.colLayer0Border
|
||||
radius: Appearance.rounding.screenRounding - Appearance.sizes.hyprlandGapsOut + 1
|
||||
|
||||
ColumnLayout {
|
||||
|
||||
@@ -16,7 +16,7 @@ Item {
|
||||
property int todoListItemPadding: 8
|
||||
property int listBottomPadding: 80
|
||||
|
||||
Flickable {
|
||||
StyledFlickable {
|
||||
id: flickable
|
||||
anchors.fill: parent
|
||||
contentHeight: columnLayout.height
|
||||
|
||||
@@ -40,7 +40,7 @@ Item {
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
ListView {
|
||||
StyledListView {
|
||||
id: listView
|
||||
model: root.appPwNodes
|
||||
clip: true
|
||||
@@ -187,7 +187,7 @@ Item {
|
||||
Layout.rightMargin: dialogMargins
|
||||
}
|
||||
|
||||
Flickable {
|
||||
StyledFlickable {
|
||||
id: dialogFlickable
|
||||
Layout.fillWidth: true
|
||||
clip: true
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//@ pragma UseQApplication
|
||||
//@ pragma Env QS_NO_RELOAD_POPUP=1
|
||||
//@ pragma Env QT_QUICK_CONTROLS_STYLE=Basic
|
||||
//@ pragma Env QT_QUICK_FLICKABLE_WHEEL_DECELERATION=10000
|
||||
|
||||
// Adjust this to make it smaller or larger
|
||||
//@ pragma Env QT_SCALE_FACTOR=1
|
||||
|
||||
@@ -6,11 +6,15 @@ import qs.modules.common
|
||||
import qs
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import QtQuick
|
||||
import "./ai/"
|
||||
|
||||
/**
|
||||
* Basic service to handle LLM chats. Supports Google's and OpenAI's API formats.
|
||||
* Supports Gemini and OpenAI models.
|
||||
* Limitations:
|
||||
* - For now functions only work with Gemini API format
|
||||
*/
|
||||
Singleton {
|
||||
id: root
|
||||
@@ -19,10 +23,19 @@ Singleton {
|
||||
property Component aiModelComponent: AiModel {}
|
||||
property Component geminiApiStrategy: GeminiApiStrategy {}
|
||||
property Component openaiApiStrategy: OpenAiApiStrategy {}
|
||||
property Component mistralApiStrategy: MistralApiStrategy {}
|
||||
readonly property string interfaceRole: "interface"
|
||||
readonly property string apiKeyEnvVarName: "API_KEY"
|
||||
|
||||
property string systemPrompt: Config.options?.ai?.systemPrompt ?? ""
|
||||
property string systemPrompt: {
|
||||
let prompt = Config.options?.ai?.systemPrompt ?? "";
|
||||
for (let key in root.promptSubstitutions) {
|
||||
// prompt = prompt.replaceAll(key, root.promptSubstitutions[key]);
|
||||
// QML/JS doesn't support replaceAll, so use split/join
|
||||
prompt = prompt.split(key).join(root.promptSubstitutions[key]);
|
||||
}
|
||||
return prompt;
|
||||
}
|
||||
// property var messages: []
|
||||
property var messageIDs: []
|
||||
property var messageByID: ({})
|
||||
@@ -49,7 +62,7 @@ Singleton {
|
||||
}
|
||||
|
||||
function safeModelName(modelName) {
|
||||
return modelName.replace(/:/g, "_").replace(/\./g, "_")
|
||||
return modelName.replace(/:/g, "_").replace(/\./g, "_").replace(/ /g, "-").replace(/\//g, "-")
|
||||
}
|
||||
|
||||
property list<var> defaultPrompts: []
|
||||
@@ -57,64 +70,169 @@ Singleton {
|
||||
property list<var> promptFiles: [...defaultPrompts, ...userPrompts]
|
||||
property list<var> savedChats: []
|
||||
|
||||
property var promptSubstitutions: {
|
||||
"{DISTRO}": SystemInfo.distroName,
|
||||
"{DATETIME}": `${DateTime.time}, ${DateTime.collapsedCalendarFormat}`,
|
||||
"{WINDOWCLASS}": ToplevelManager.activeToplevel?.appId ?? "Unknown",
|
||||
"{DE}": `${SystemInfo.desktopEnvironment} (${SystemInfo.windowingSystem})`
|
||||
}
|
||||
|
||||
// Gemini: https://ai.google.dev/gemini-api/docs/function-calling
|
||||
// OpenAI: https://platform.openai.com/docs/guides/function-calling
|
||||
property string currentTool: Config?.options.ai.tool ?? "search"
|
||||
property var tools: {
|
||||
"gemini": [{"functionDeclarations": [
|
||||
{
|
||||
"name": "switch_to_search_mode",
|
||||
"description": "Search the web",
|
||||
},
|
||||
{
|
||||
"name": "get_shell_config",
|
||||
"description": "Get the desktop shell config file contents",
|
||||
},
|
||||
{
|
||||
"name": "set_shell_config",
|
||||
"description": "Set a field in the desktop graphical shell config file. Must only be used after `get_shell_config`.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string",
|
||||
"description": "The key to set, e.g. `bar.borderless`. MUST NOT BE GUESSED, use `get_shell_config` to see what keys are available before setting.",
|
||||
"gemini": {
|
||||
"functions": [{"functionDeclarations": [
|
||||
{
|
||||
"name": "switch_to_search_mode",
|
||||
"description": "Search the web",
|
||||
},
|
||||
{
|
||||
"name": "get_shell_config",
|
||||
"description": "Get the desktop shell config file contents",
|
||||
},
|
||||
{
|
||||
"name": "set_shell_config",
|
||||
"description": "Set a field in the desktop graphical shell config file. Must only be used after `get_shell_config`.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string",
|
||||
"description": "The key to set, e.g. `bar.borderless`. MUST NOT BE GUESSED, use `get_shell_config` to see what keys are available before setting.",
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "The value to set, e.g. `true`"
|
||||
}
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "The value to set, e.g. `true`"
|
||||
"required": ["key", "value"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "run_shell_command",
|
||||
"description": "Run a shell command in bash and get its output. Use this only for quick commands that don't require user interaction. For commands that require interaction, ask the user to run manually instead.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "The bash command to run",
|
||||
},
|
||||
},
|
||||
"required": ["command"]
|
||||
}
|
||||
},
|
||||
]}],
|
||||
"search": [{
|
||||
"google_search": {}
|
||||
}],
|
||||
"none": []
|
||||
},
|
||||
"openai": {
|
||||
"functions": [
|
||||
{
|
||||
"name": "switch_to_search_mode",
|
||||
"description": "Search the web",
|
||||
},
|
||||
{
|
||||
"name": "get_shell_config",
|
||||
"description": "Get the desktop shell config file contents",
|
||||
},
|
||||
{
|
||||
"name": "set_shell_config",
|
||||
"description": "Set a field in the desktop graphical shell config file. Must only be used after `get_shell_config`.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string",
|
||||
"description": "The key to set, e.g. `bar.borderless`. MUST NOT BE GUESSED, use `get_shell_config` to see what keys are available before setting.",
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "The value to set, e.g. `true`"
|
||||
}
|
||||
},
|
||||
"required": ["key", "value"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "run_shell_command",
|
||||
"description": "Run a shell command in bash and get its output. Use this only for quick commands that don't require user interaction. For commands that require interaction, ask the user to run manually instead.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "The bash command to run",
|
||||
},
|
||||
},
|
||||
"required": ["command"]
|
||||
}
|
||||
},
|
||||
],
|
||||
"search": [],
|
||||
"none": [],
|
||||
},
|
||||
"mistral": {
|
||||
"functions": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_shell_config",
|
||||
"description": "Get the desktop shell config file contents",
|
||||
"parameters": {}
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "set_shell_config",
|
||||
"description": "Set a field in the desktop graphical shell config file. Must only be used after `get_shell_config`.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string",
|
||||
"description": "The key to set, e.g. `bar.borderless`. MUST NOT BE GUESSED, use `get_shell_config` to see what keys are available before setting.",
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "The value to set, e.g. `true`"
|
||||
}
|
||||
},
|
||||
"required": ["key", "value"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "run_shell_command",
|
||||
"description": "Run a shell command in bash and get its output. Use this only for quick commands that don't require user interaction. For commands that require interaction, ask the user to run manually instead.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "The bash command to run",
|
||||
},
|
||||
},
|
||||
"required": ["command"]
|
||||
}
|
||||
},
|
||||
"required": ["key", "value"]
|
||||
}
|
||||
},
|
||||
]}],
|
||||
"openai": [
|
||||
{
|
||||
"type": "function",
|
||||
"name": "get_shell_config",
|
||||
"description": "Get the current shell configuration.",
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "set_shell_config",
|
||||
"description": "Set a field in the desktop graphical shell config file. Must only be used after `get_shell_config`.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string",
|
||||
"description": "The key to set, e.g. `bar.borderless`. MUST NOT BE GUESSED, use `get_shell_config` to see what keys are available before setting.",
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "The value to set, e.g. `true`"
|
||||
}
|
||||
},
|
||||
"required": ["key", "value"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
"search": [],
|
||||
"none": [],
|
||||
}
|
||||
}
|
||||
property list<var> availableTools: Object.keys(root.tools[models[currentModelId]?.api_format])
|
||||
property var toolDescriptions: {
|
||||
"functions": Translation.tr("Commands, edit configs, search.\nTakes an extra turn to switch to search mode if that's needed"),
|
||||
"search": Translation.tr("Gives the model search capabilities (immediately)"),
|
||||
"none": Translation.tr("Disable tools")
|
||||
}
|
||||
|
||||
// Model properties:
|
||||
@@ -128,13 +246,12 @@ Singleton {
|
||||
// - key_get_link: Link to get an API key
|
||||
// - key_get_description: Description of pricing and how to get an API key
|
||||
// - api_format: The API format of the model. Can be "openai" or "gemini". Default is "openai".
|
||||
// - tools: List of tools that the model can use. Each tool is an object with the tool name as the key and an empty object as the value.
|
||||
// - extraParams: Extra parameters to be passed to the model. This is a JSON object.
|
||||
property var models: {
|
||||
"gemini-2.0-flash-search": aiModelComponent.createObject(this, {
|
||||
"name": "Gemini 2.0 Flash (Search)",
|
||||
"gemini-2.0-flash": aiModelComponent.createObject(this, {
|
||||
"name": "Gemini 2.0 Flash",
|
||||
"icon": "google-gemini-symbolic",
|
||||
"description": Translation.tr("Online | Google's model\nGives up-to-date information with search."),
|
||||
"description": Translation.tr("Online | Google's model\nFast, can perform searches for up-to-date information"),
|
||||
"homepage": "https://aistudio.google.com",
|
||||
"endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:streamGenerateContent",
|
||||
"model": "gemini-2.0-flash",
|
||||
@@ -143,28 +260,11 @@ Singleton {
|
||||
"key_get_link": "https://aistudio.google.com/app/apikey",
|
||||
"key_get_description": Translation.tr("**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key"),
|
||||
"api_format": "gemini",
|
||||
"tools": [{
|
||||
"google_search": {}
|
||||
}]
|
||||
}),
|
||||
"gemini-2.0-flash-tools": aiModelComponent.createObject(this, {
|
||||
"name": "Gemini 2.0 Flash (Tools)",
|
||||
"gemini-2.5-flash": aiModelComponent.createObject(this, {
|
||||
"name": "Gemini 2.5 Flash",
|
||||
"icon": "google-gemini-symbolic",
|
||||
"description": Translation.tr("Experimental | Online | Google's model\nCan do a little more but takes an extra turn to perform search"),
|
||||
"homepage": "https://aistudio.google.com",
|
||||
"endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:streamGenerateContent",
|
||||
"model": "gemini-2.0-flash",
|
||||
"requires_key": true,
|
||||
"key_id": "gemini",
|
||||
"key_get_link": "https://aistudio.google.com/app/apikey",
|
||||
"key_get_description": Translation.tr("**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key"),
|
||||
"api_format": "gemini",
|
||||
"tools": root.tools["gemini"],
|
||||
}),
|
||||
"gemini-2.5-flash-search": aiModelComponent.createObject(this, {
|
||||
"name": "Gemini 2.5 Flash (Search)",
|
||||
"icon": "google-gemini-symbolic",
|
||||
"description": Translation.tr("Online | Google's model\nGives up-to-date information with search."),
|
||||
"description": Translation.tr("Online | Google's model\nNewer model that's slower than its predecessor but should deliver higher quality answers"),
|
||||
"homepage": "https://aistudio.google.com",
|
||||
"endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:streamGenerateContent",
|
||||
"model": "gemini-2.5-flash",
|
||||
@@ -173,44 +273,24 @@ Singleton {
|
||||
"key_get_link": "https://aistudio.google.com/app/apikey",
|
||||
"key_get_description": Translation.tr("**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key"),
|
||||
"api_format": "gemini",
|
||||
"tools": [{
|
||||
"google_search": {}
|
||||
}]
|
||||
}),
|
||||
"gemini-2.5-flash-tools": aiModelComponent.createObject(this, {
|
||||
"name": "Gemini 2.5 Flash (Tools)",
|
||||
"gemini-2.5-flash-pro": aiModelComponent.createObject(this, {
|
||||
"name": "Gemini 2.5 Pro",
|
||||
"icon": "google-gemini-symbolic",
|
||||
"description": Translation.tr("Experimental | Online | Google's model\nCan do a little more but takes an extra turn to perform search"),
|
||||
"description": Translation.tr("Online | Google's model\nGoogle's state-of-the-art multipurpose model that excels at coding and complex reasoning tasks."),
|
||||
"homepage": "https://aistudio.google.com",
|
||||
"endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:streamGenerateContent",
|
||||
"model": "gemini-2.5-flash",
|
||||
"endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:streamGenerateContent",
|
||||
"model": "gemini-2.5-pro",
|
||||
"requires_key": true,
|
||||
"key_id": "gemini",
|
||||
"key_get_link": "https://aistudio.google.com/app/apikey",
|
||||
"key_get_description": Translation.tr("**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key"),
|
||||
"api_format": "gemini",
|
||||
"tools": root.tools["gemini"],
|
||||
}),
|
||||
"gemini-2.5-flash-lite-search": aiModelComponent.createObject(this, {
|
||||
"name": "Gemini 2.5 Flash-Lite (Search)",
|
||||
"icon": "google-gemini-symbolic",
|
||||
"description": Translation.tr("Experimental | Online | Google's model\nA Gemini 2.5 Flash model optimized for cost-efficiency and high throughput."),
|
||||
"homepage": "https://aistudio.google.com",
|
||||
"endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-lite:streamGenerateContent",
|
||||
"model": "gemini-2.5-flash-lite",
|
||||
"requires_key": true,
|
||||
"key_id": "gemini",
|
||||
"key_get_link": "https://aistudio.google.com/app/apikey",
|
||||
"key_get_description": Translation.tr("**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key"),
|
||||
"api_format": "gemini",
|
||||
"tools": [{
|
||||
"google_search": {}
|
||||
}]
|
||||
}),
|
||||
"gemini-2.5-flash-lite": aiModelComponent.createObject(this, {
|
||||
"name": "Gemini 2.5 Flash-Lite",
|
||||
"icon": "google-gemini-symbolic",
|
||||
"description": Translation.tr("Experimental | Online | Google's model\nA Gemini 2.5 Flash model optimized for cost-efficiency and high throughput."),
|
||||
"description": Translation.tr("Online | Google's model\nA Gemini 2.5 Flash model optimized for cost-efficiency and high throughput."),
|
||||
"homepage": "https://aistudio.google.com",
|
||||
"endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-lite:streamGenerateContent",
|
||||
"model": "gemini-2.5-flash-lite",
|
||||
@@ -219,19 +299,19 @@ Singleton {
|
||||
"key_get_link": "https://aistudio.google.com/app/apikey",
|
||||
"key_get_description": Translation.tr("**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key"),
|
||||
"api_format": "gemini",
|
||||
"tools": root.tools["gemini"],
|
||||
}),
|
||||
"openrouter-llama4-maverick": aiModelComponent.createObject(this, {
|
||||
"name": "Llama 4 Maverick",
|
||||
"icon": "ollama-symbolic",
|
||||
"description": Translation.tr("Online via %1 | %2's model").arg("OpenRouter").arg("Meta"),
|
||||
"homepage": "https://openrouter.ai/meta-llama/llama-4-maverick:free",
|
||||
"endpoint": "https://openrouter.ai/api/v1/chat/completions",
|
||||
"model": "meta-llama/llama-4-maverick:free",
|
||||
"mistral-medium-3": aiModelComponent.createObject(this, {
|
||||
"name": "Mistral Medium 3",
|
||||
"icon": "mistral-symbolic",
|
||||
"description": Translation.tr("Online | %1's model | Delivers fast, responsive and well-formatted answers. Disadvantages: not very eager to do stuff; might make up unknown function calls").arg("Mistral"),
|
||||
"homepage": "https://mistral.ai/news/mistral-medium-3",
|
||||
"endpoint": "https://api.mistral.ai/v1/chat/completions",
|
||||
"model": "mistral-medium-2505",
|
||||
"requires_key": true,
|
||||
"key_id": "openrouter",
|
||||
"key_get_link": "https://openrouter.ai/settings/keys",
|
||||
"key_get_description": Translation.tr("**Pricing**: free. Data use policy varies depending on your OpenRouter account settings.\n\n**Instructions**: Log into OpenRouter account, go to Keys on the topright menu, click Create API Key"),
|
||||
"key_id": "mistral",
|
||||
"key_get_link": "https://console.mistral.ai/api-keys",
|
||||
"key_get_description": Translation.tr("**Instructions**: Log into Mistral account, go to Keys on the sidebar, click Create new key"),
|
||||
"api_format": "mistral",
|
||||
}),
|
||||
"openrouter-deepseek-r1": aiModelComponent.createObject(this, {
|
||||
"name": "DeepSeek R1",
|
||||
@@ -252,9 +332,21 @@ Singleton {
|
||||
property var apiStrategies: {
|
||||
"openai": openaiApiStrategy.createObject(this),
|
||||
"gemini": geminiApiStrategy.createObject(this),
|
||||
"mistral": mistralApiStrategy.createObject(this),
|
||||
}
|
||||
property ApiStrategy currentApiStrategy: apiStrategies[models[currentModelId]?.api_format || "openai"]
|
||||
|
||||
Connections {
|
||||
target: Config
|
||||
function onReadyChanged() {
|
||||
if (!Config.ready) return;
|
||||
(Config?.options.ai?.extraModels ?? []).forEach(model => {
|
||||
const safeModelName = root.safeModelName(model["model"]);
|
||||
root.addModel(safeModelName, model)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
setModel(currentModelId, false, false); // Do necessary setup for model
|
||||
}
|
||||
@@ -280,6 +372,10 @@ Singleton {
|
||||
return result;
|
||||
}
|
||||
|
||||
function addModel(modelName, data) {
|
||||
root.models[modelName] = aiModelComponent.createObject(this, data);
|
||||
}
|
||||
|
||||
Process {
|
||||
id: getOllamaModels
|
||||
running: true
|
||||
@@ -292,7 +388,7 @@ Singleton {
|
||||
root.modelList = [...root.modelList, ...dataJson];
|
||||
dataJson.forEach(model => {
|
||||
const safeModelName = root.safeModelName(model);
|
||||
root.models[safeModelName] = aiModelComponent.createObject(this, {
|
||||
root.addModel(safeModelName, {
|
||||
"name": guessModelName(model),
|
||||
"icon": guessModelLogo(model),
|
||||
"description": Translation.tr("Local Ollama model | %1").arg(model),
|
||||
@@ -398,8 +494,8 @@ Singleton {
|
||||
|
||||
function addApiKeyAdvice(model) {
|
||||
root.addMessage(
|
||||
Translation.tr('To set an API key, pass it with the command\n\nTo view the key, pass "get" with the command<br/>\n\n### For %1:\n\n**Link**: %2\n\n%3')
|
||||
.arg(model.name).arg(model.key_get_link).arg(model.key_get_description ?? Translation.tr("<i>No further instruction provided</i>")),
|
||||
Translation.tr('To set an API key, pass it with the %4 command\n\nTo view the key, pass "get" with the command<br/>\n\n### For %1:\n\n**Link**: %2\n\n%3')
|
||||
.arg(model.name).arg(model.key_get_link).arg(model.key_get_description ?? Translation.tr("<i>No further instruction provided</i>")).arg("/key"),
|
||||
Ai.interfaceRole
|
||||
);
|
||||
}
|
||||
@@ -435,6 +531,15 @@ Singleton {
|
||||
if (feedback) root.addMessage(Translation.tr("Invalid model. Supported: \n```\n") + modelList.join("\n```\n```\n"), Ai.interfaceRole) + "\n```"
|
||||
}
|
||||
}
|
||||
|
||||
function setTool(tool) {
|
||||
if (!root.tools[models[currentModelId]?.api_format] || !(tool in root.tools[models[currentModelId]?.api_format])) {
|
||||
root.addMessage(Translation.tr("Invalid tool. Supported tools:\n- %1").arg(root.availableTools.join("\n- ")), root.interfaceRole);
|
||||
return false;
|
||||
}
|
||||
Config.options.ai.tool = tool;
|
||||
return true;
|
||||
}
|
||||
|
||||
function getTemperature() {
|
||||
return root.temperature;
|
||||
@@ -493,7 +598,7 @@ Singleton {
|
||||
|
||||
Process {
|
||||
id: requester
|
||||
property var baseCommand: ["bash", "-c"]
|
||||
property list<string> baseCommand: ["bash", "-c"]
|
||||
property AiMessageData message
|
||||
property ApiStrategy currentStrategy
|
||||
|
||||
@@ -518,7 +623,7 @@ Singleton {
|
||||
const endpoint = root.currentApiStrategy.buildEndpoint(model);
|
||||
const messageArray = root.messageIDs.map(id => root.messageByID[id]);
|
||||
const filteredMessageArray = messageArray.filter(message => message.role !== Ai.interfaceRole);
|
||||
const data = root.currentApiStrategy.buildRequestData(model, filteredMessageArray, root.systemPrompt, root.temperature);
|
||||
const data = root.currentApiStrategy.buildRequestData(model, filteredMessageArray, root.systemPrompt, root.temperature, root.tools[model.api_format][root.currentTool]);
|
||||
// console.log("[Ai] Request data: ", JSON.stringify(data, null, 2));
|
||||
|
||||
let requestHeaders = {
|
||||
@@ -563,9 +668,9 @@ Singleton {
|
||||
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
// console.log("[Ai] Raw response line: ", data);
|
||||
if (data.length === 0) return;
|
||||
if (requester.message.thinking) requester.message.thinking = false;
|
||||
// console.log("[Ai] Raw response line: ", data);
|
||||
|
||||
// Handle response line
|
||||
try {
|
||||
@@ -573,7 +678,8 @@ Singleton {
|
||||
// console.log("[Ai] Parsed response result: ", JSON.stringify(result, null, 2));
|
||||
|
||||
if (result.functionCall) {
|
||||
root.handleFunctionCall(result.functionCall.name, result.functionCall.args);
|
||||
requester.message.functionCall = result.functionCall;
|
||||
root.handleFunctionCall(result.functionCall.name, result.functionCall.args, requester.message);
|
||||
}
|
||||
if (result.tokenUsage) {
|
||||
root.tokenCount.input = result.tokenUsage.input;
|
||||
@@ -614,38 +720,72 @@ Singleton {
|
||||
requester.makeRequest();
|
||||
}
|
||||
|
||||
function addFunctionOutputMessage(name, output) {
|
||||
const aiMessage = aiMessageComponent.createObject(root, {
|
||||
function createFunctionOutputMessage(name, output, includeOutputInChat = true) {
|
||||
return aiMessageComponent.createObject(root, {
|
||||
"role": "user",
|
||||
"content": `[[ Output of ${name} ]]`,
|
||||
"rawContent": `[[ Output of ${name} ]]`,
|
||||
"content": `[[ Output of ${name} ]]${includeOutputInChat ? ("\n\n<think>\n" + output + "\n</think>") : ""}`,
|
||||
"rawContent": `[[ Output of ${name} ]]${includeOutputInChat ? ("\n\n<think>\n" + output + "\n</think>") : ""}`,
|
||||
"functionName": name,
|
||||
"functionResponse": output,
|
||||
"thinking": false,
|
||||
"done": true,
|
||||
"visibleToUser": false,
|
||||
// "visibleToUser": false,
|
||||
});
|
||||
// console.log("Adding function output message: ", JSON.stringify(aiMessage));
|
||||
}
|
||||
|
||||
function addFunctionOutputMessage(name, output) {
|
||||
const aiMessage = createFunctionOutputMessage(name, output);
|
||||
const id = idForMessage(aiMessage);
|
||||
root.messageIDs = [...root.messageIDs, id];
|
||||
root.messageByID[id] = aiMessage;
|
||||
}
|
||||
|
||||
function handleFunctionCall(name, args) {
|
||||
function rejectCommand(message: AiMessageData) {
|
||||
if (!message.functionPending) return;
|
||||
message.functionPending = false; // User decided, no more "thinking"
|
||||
addFunctionOutputMessage(message.functionName, Translation.tr("Command rejected by user"))
|
||||
}
|
||||
|
||||
function approveCommand(message: AiMessageData) {
|
||||
if (!message.functionPending) return;
|
||||
message.functionPending = false; // User decided, no more "thinking"
|
||||
|
||||
const responseMessage = createFunctionOutputMessage(message.functionName, "", false);
|
||||
const id = idForMessage(responseMessage);
|
||||
root.messageIDs = [...root.messageIDs, id];
|
||||
root.messageByID[id] = responseMessage;
|
||||
|
||||
commandExecutionProc.message = responseMessage;
|
||||
commandExecutionProc.baseMessageContent = responseMessage.content;
|
||||
commandExecutionProc.shellCommand = message.functionCall.args.command;
|
||||
commandExecutionProc.running = true; // Start the command execution
|
||||
}
|
||||
|
||||
Process {
|
||||
id: commandExecutionProc
|
||||
property string shellCommand: ""
|
||||
property AiMessageData message
|
||||
property string baseMessageContent: ""
|
||||
command: ["bash", "-c", shellCommand]
|
||||
stdout: SplitParser {
|
||||
onRead: (output) => {
|
||||
commandExecutionProc.message.functionResponse += output + "\n\n";
|
||||
const updatedContent = commandExecutionProc.baseMessageContent + `\n\n<think>\n<tt>${commandExecutionProc.message.functionResponse}</tt>\n</think>`;
|
||||
commandExecutionProc.message.rawContent = updatedContent;
|
||||
commandExecutionProc.message.content = updatedContent;
|
||||
}
|
||||
}
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
commandExecutionProc.message.functionResponse += `[[ Command exited with code ${exitCode} (${exitStatus}) ]]\n`;
|
||||
requester.makeRequest(); // Continue
|
||||
}
|
||||
}
|
||||
|
||||
function handleFunctionCall(name, args: var, message: AiMessageData) {
|
||||
if (name === "switch_to_search_mode") {
|
||||
const modelId = root.currentModelId;
|
||||
if (modelId.endsWith("-tools")) {
|
||||
const searchModelId = modelId.replace(/-tools$/, "-search");
|
||||
if (root.modelList.indexOf(searchModelId) !== -1) {
|
||||
root.setModel(searchModelId, false);
|
||||
root.postResponseHook = () => root.setModel(modelId, false);
|
||||
} else {
|
||||
root.addMessage(Translation.tr("No corresponding search model found for %1").arg(modelId), Ai.interfaceRole);
|
||||
}
|
||||
} else {
|
||||
root.addMessage(Translation.tr("Cannot switch to search mode from %1").arg(root.currentModelId), Ai.interfaceRole);
|
||||
return;
|
||||
}
|
||||
root.currentTool = "search"
|
||||
root.postResponseHook = () => { root.currentTool = "functions" }
|
||||
addFunctionOutputMessage(name, Translation.tr("Switched to search mode. Continue with the user's request."))
|
||||
requester.makeRequest();
|
||||
} else if (name === "get_shell_config") {
|
||||
@@ -660,6 +800,15 @@ Singleton {
|
||||
const key = args.key;
|
||||
const value = args.value;
|
||||
Config.setNestedValue(key, value);
|
||||
} else if (name === "run_shell_command") {
|
||||
if (!args.command || args.command.length === 0) {
|
||||
addFunctionOutputMessage(name, Translation.tr("Invalid arguments. Must provide `command`."));
|
||||
return;
|
||||
}
|
||||
const contentToAppend = `\n\n**Command execution request**\n\n\`\`\`command\n${args.command}\n\`\`\``;
|
||||
message.rawContent += contentToAppend;
|
||||
message.content += contentToAppend;
|
||||
message.functionPending = true; // Use thinking to indicate the command is waiting for approval
|
||||
}
|
||||
else root.addMessage(Translation.tr("Unknown function call: %1").arg(name), "assistant");
|
||||
}
|
||||
|
||||
@@ -69,10 +69,11 @@ Singleton {
|
||||
|
||||
Process {
|
||||
id: getClients
|
||||
command: ["bash", "-c", "hyprctl clients -j | jq -c"]
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
root.windowList = JSON.parse(data);
|
||||
command: ["bash", "-c", "hyprctl clients -j"]
|
||||
stdout: StdioCollector {
|
||||
id: clientsCollector
|
||||
onStreamFinished: {
|
||||
root.windowList = JSON.parse(clientsCollector.text)
|
||||
let tempWinByAddress = {};
|
||||
for (var i = 0; i < root.windowList.length; ++i) {
|
||||
var win = root.windowList[i];
|
||||
@@ -86,30 +87,33 @@ Singleton {
|
||||
|
||||
Process {
|
||||
id: getMonitors
|
||||
command: ["bash", "-c", "hyprctl monitors -j | jq -c"]
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
root.monitors = JSON.parse(data);
|
||||
command: ["bash", "-c", "hyprctl monitors -j"]
|
||||
stdout: StdioCollector {
|
||||
id: monitorsCollector
|
||||
onStreamFinished: {
|
||||
root.monitors = JSON.parse(monitorsCollector.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: getLayers
|
||||
command: ["bash", "-c", "hyprctl layers -j | jq -c"]
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
root.layers = JSON.parse(data);
|
||||
command: ["bash", "-c", "hyprctl layers -j"]
|
||||
stdout: StdioCollector {
|
||||
id: layersCollector
|
||||
onStreamFinished: {
|
||||
root.layers = JSON.parse(layersCollector.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: getWorkspaces
|
||||
command: ["bash", "-c", "hyprctl workspaces -j | jq -c"]
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
root.workspaces = JSON.parse(data);
|
||||
command: ["bash", "-c", "hyprctl workspaces -j"]
|
||||
stdout: StdioCollector {
|
||||
id: workspacesCollector
|
||||
onStreamFinished: {
|
||||
root.workspaces = JSON.parse(workspacesCollector.text);
|
||||
let tempWorkspaceById = {};
|
||||
for (var i = 0; i < root.workspaces.length; ++i) {
|
||||
var ws = root.workspaces[i];
|
||||
@@ -123,10 +127,11 @@ Singleton {
|
||||
|
||||
Process {
|
||||
id: getActiveWorkspace
|
||||
command: ["bash", "-c", "hyprctl activeworkspace -j | jq -c"]
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
root.activeWorkspace = JSON.parse(data);
|
||||
command: ["bash", "-c", "hyprctl activeworkspace -j"]
|
||||
stdout: StdioCollector {
|
||||
id: activeWorkspaceCollector
|
||||
onStreamFinished: {
|
||||
root.activeWorkspace = JSON.parse(activeWorkspaceCollector.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Hyprland
|
||||
import qs.modules.common
|
||||
|
||||
/**
|
||||
* Exposes the active Hyprland Xkb keyboard layout name and code for indicators.
|
||||
@@ -16,7 +17,6 @@ Singleton {
|
||||
property string currentLayoutName: ""
|
||||
property string currentLayoutCode: ""
|
||||
// For the service
|
||||
property string targetDeviceName: "hl-virtual-keyboard"
|
||||
property var baseLayoutFilePath: "/usr/share/X11/xkb/rules/base.lst"
|
||||
property bool needsLayoutRefresh: false
|
||||
|
||||
@@ -71,7 +71,7 @@ Singleton {
|
||||
id: devicesCollector
|
||||
onStreamFinished: {
|
||||
const parsedOutput = JSON.parse(devicesCollector.text);
|
||||
const hyprlandKeyboard = parsedOutput["keyboards"].find(kb => kb.name === root.targetDeviceName);
|
||||
const hyprlandKeyboard = parsedOutput["keyboards"].find(kb => kb.main === true);
|
||||
root.layoutCodes = hyprlandKeyboard["layout"].split(",");
|
||||
root.currentLayoutName = hyprlandKeyboard["active_keymap"];
|
||||
// console.log("[HyprlandXkb] Fetched | Layouts (multiple: " + (root.layouts.length > 1) + "): "
|
||||
@@ -85,8 +85,6 @@ Singleton {
|
||||
target: Hyprland
|
||||
function onRawEvent(event) {
|
||||
if (event.name === "activelayout") {
|
||||
// We're triggering refresh here because Hyprland virtual kb after a config reload disappears
|
||||
// from `hyprctl devices` and it only comes back at the next activelayout event.
|
||||
if (root.needsLayoutRefresh) {
|
||||
root.needsLayoutRefresh = false;
|
||||
fetchLayoutsProc.running = true;
|
||||
@@ -97,9 +95,10 @@ Singleton {
|
||||
|
||||
// Update when layout might have changed
|
||||
const dataString = event.data;
|
||||
if (!dataString.startsWith(root.targetDeviceName))
|
||||
return;
|
||||
root.currentLayoutName = dataString.split(",")[1];
|
||||
|
||||
// Update layout for on-screen keyboard (osk)
|
||||
Config.options.osk.layout = root.currentLayoutName;
|
||||
} else if (event.name == "configreloaded") {
|
||||
// Mark layout code list to be updated when config is reloaded
|
||||
root.needsLayoutRefresh = true;
|
||||
|
||||
@@ -34,11 +34,9 @@ Singleton {
|
||||
property string urgency: notification?.urgency.toString() ?? "normal"
|
||||
property Timer timer
|
||||
|
||||
readonly property Connections conn: Connections {
|
||||
target: wrapper?.notification?.Component ?? root // stupid warning aaaaaaa
|
||||
|
||||
function onDestruction(): void {
|
||||
wrapper.destroy();
|
||||
onNotificationChanged: {
|
||||
if (notification === null) {
|
||||
root.discardNotification(notificationId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ Singleton {
|
||||
property string bugReportUrl: ""
|
||||
property string privacyPolicyUrl: ""
|
||||
property string logo: ""
|
||||
property string desktopEnvironment: ""
|
||||
property string windowingSystem: ""
|
||||
|
||||
Timer {
|
||||
triggeredOnStart: true
|
||||
@@ -83,6 +85,20 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: getDesktopEnvironment
|
||||
running: true
|
||||
command: ["bash", "-c", "echo $XDG_CURRENT_DESKTOP,$WAYLAND_DISPLAY"]
|
||||
stdout: StdioCollector {
|
||||
id: deCollector
|
||||
onStreamFinished: {
|
||||
const [desktop, wayland] = deCollector.text.split(",")
|
||||
root.desktopEnvironment = desktop.trim()
|
||||
root.windowingSystem = wayland.trim().length > 0 ? "Wayland" : "X11" // Are there others? 🤔
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: fileOsRelease
|
||||
path: "/etc/os-release"
|
||||
|
||||
@@ -14,7 +14,8 @@ QtObject {
|
||||
property var annotationSources: []
|
||||
property list<string> searchQueries: []
|
||||
property string functionName
|
||||
property string functionCall
|
||||
property var functionCall
|
||||
property string functionResponse
|
||||
property bool functionPending: false
|
||||
property bool visibleToUser: true
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import QtQuick;
|
||||
* - key_get_link: Link to get an API key
|
||||
* - key_get_description: Description of pricing and how to get an API key
|
||||
* - api_format: The API format of the model. Can be "openai" or "gemini". Default is "openai".
|
||||
* - tools: List of tools that the model can use.
|
||||
* - extraParams: Extra parameters to be passed to the model. This is a JSON object.
|
||||
*/
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import QtQuick
|
||||
|
||||
QtObject {
|
||||
function buildEndpoint(model: AiModel): string { throw new Error("Not implemented") }
|
||||
function buildRequestData(model: AiModel, messages, systemPrompt: string, temperature: real) { throw new Error("Not implemented") }
|
||||
function buildRequestData(model: AiModel, messages, systemPrompt: string, temperature: real, tools: list<var>) { throw new Error("Not implemented") }
|
||||
function buildAuthorizationHeader(apiKeyEnvVarName: string): string { throw new Error("Not implemented") }
|
||||
function parseResponseLine(line: string, message: AiMessageData) { throw new Error("Not implemented") }
|
||||
function onRequestFinished(message: AiMessageData): var { return {} } // Default: no special handling
|
||||
|
||||
@@ -5,17 +5,16 @@ ApiStrategy {
|
||||
|
||||
function buildEndpoint(model: AiModel): string {
|
||||
const result = model.endpoint + `?key=\$\{${root.apiKeyEnvVarName}\}`
|
||||
console.log("[AI] Endpoint: " + result);
|
||||
// console.log("[AI] Endpoint: " + result);
|
||||
return result;
|
||||
}
|
||||
|
||||
function buildRequestData(model: AiModel, messages, systemPrompt: string, temperature: real) {
|
||||
const tools = model.tools ?? [];
|
||||
function buildRequestData(model: AiModel, messages, systemPrompt: string, temperature: real, tools: list<var>) {
|
||||
let baseData = {
|
||||
"contents": messages.map(message => {
|
||||
const geminiApiRoleName = (message.role === "assistant") ? "model" : message.role;
|
||||
const usingSearch = tools[0].google_search != undefined
|
||||
if (!usingSearch && message.functionCall != undefined && message.functionCall.length > 0) {
|
||||
const usingSearch = tools[0]?.google_search !== undefined
|
||||
if (!usingSearch && message.functionCall != undefined && message.functionName.length > 0) {
|
||||
return {
|
||||
"role": geminiApiRoleName,
|
||||
"parts": [{
|
||||
@@ -25,7 +24,7 @@ ApiStrategy {
|
||||
}]
|
||||
}
|
||||
}
|
||||
if (!usingSearch && message.functionResponse != undefined && message.functionResponse.length > 0) {
|
||||
if (!usingSearch && message.functionResponse != undefined && message.functionName.length > 0) {
|
||||
return {
|
||||
"role": geminiApiRoleName,
|
||||
"parts": [{
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import QtQuick
|
||||
|
||||
ApiStrategy {
|
||||
property bool isReasoning: false
|
||||
|
||||
function buildEndpoint(model: AiModel): string {
|
||||
// console.log("[AI] Endpoint: " + model.endpoint);
|
||||
return model.endpoint;
|
||||
}
|
||||
|
||||
function buildRequestData(model: AiModel, messages, systemPrompt: string, temperature: real, tools: list<var>) {
|
||||
let baseData = {
|
||||
"model": model.model,
|
||||
"messages": [
|
||||
{role: "system", content: systemPrompt},
|
||||
...messages.map(message => {
|
||||
const hasFunctionCall = message.functionCall != undefined && message.functionName.length > 0
|
||||
let messageData = {
|
||||
"role": message.role,
|
||||
"content": message.rawContent,
|
||||
}
|
||||
if (hasFunctionCall) {
|
||||
if (message.functionResponse?.length > 0) {
|
||||
messageData.name = message.functionName; // Does the func call also need this name? or just the func output?
|
||||
messageData.role = "tool";
|
||||
messageData.content = message.functionResponse;
|
||||
messageData.tool_call_id = message.functionCall.id
|
||||
}
|
||||
}
|
||||
return messageData
|
||||
}),
|
||||
],
|
||||
"stream": true,
|
||||
"temperature": temperature,
|
||||
"tools": tools,
|
||||
};
|
||||
// console.log("[AI] Request data: ", JSON.stringify(baseData, null, 2));
|
||||
return model.extraParams ? Object.assign({}, baseData, model.extraParams) : baseData;
|
||||
}
|
||||
|
||||
function buildAuthorizationHeader(apiKeyEnvVarName: string): string {
|
||||
return `-H "Authorization: Bearer \$\{${apiKeyEnvVarName}\}"`;
|
||||
}
|
||||
|
||||
function parseResponseLine(line, message) {
|
||||
// Remove 'data: ' prefix if present and trim whitespace
|
||||
let cleanData = line.trim();
|
||||
if (cleanData.startsWith("data:")) {
|
||||
cleanData = cleanData.slice(5).trim();
|
||||
}
|
||||
|
||||
// Handle special cases
|
||||
if (!cleanData || cleanData.startsWith(":")) return {};
|
||||
if (cleanData === "[DONE]") {
|
||||
return { finished: true };
|
||||
}
|
||||
|
||||
// Real stuff
|
||||
try {
|
||||
const dataJson = JSON.parse(cleanData);
|
||||
let newContent = "";
|
||||
|
||||
const responseContent = dataJson.choices[0]?.delta?.content || dataJson.message?.content;
|
||||
const responseReasoning = dataJson.choices[0]?.delta?.reasoning || dataJson.choices[0]?.delta?.reasoning_content;
|
||||
|
||||
// Function call
|
||||
if (dataJson.choices[0]?.delta?.tool_calls) {
|
||||
const functionCall = dataJson.choices[0].delta.tool_calls[0];
|
||||
const functionName = functionCall.function.name;
|
||||
const functionArgs = JSON.parse(functionCall.function.arguments) || {}; // Args are given as string???
|
||||
const functionId = functionCall.id;
|
||||
const newContent = `\n\n[[ Function: ${functionName}(${JSON.stringify(functionArgs, null, 2)}) ]]\n`;
|
||||
message.rawContent += newContent;
|
||||
message.content += newContent;
|
||||
message.functionName = functionName;
|
||||
message.functionCall = functionName;
|
||||
return { functionCall: { name: functionName, args: functionArgs, id: functionId } };
|
||||
}
|
||||
|
||||
// Thinking?
|
||||
if (responseContent && responseContent.length > 0) {
|
||||
if (isReasoning) {
|
||||
isReasoning = false;
|
||||
const endBlock = "\n\n</think>\n\n";
|
||||
message.content += endBlock;
|
||||
message.rawContent += endBlock;
|
||||
}
|
||||
newContent = responseContent;
|
||||
} else if (responseReasoning && responseReasoning.length > 0) {
|
||||
if (!isReasoning) {
|
||||
isReasoning = true;
|
||||
const startBlock = "\n\n<think>\n\n";
|
||||
message.rawContent += startBlock;
|
||||
message.content += startBlock;
|
||||
}
|
||||
newContent = responseReasoning;
|
||||
}
|
||||
|
||||
// Text
|
||||
message.content += newContent;
|
||||
message.rawContent += newContent;
|
||||
|
||||
if (`dataJson`.done) {
|
||||
return { finished: true };
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.log("[AI] Mistral: Could not parse line: ", e);
|
||||
message.rawContent += line;
|
||||
message.content += line;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
function onRequestFinished(message) {
|
||||
return {};
|
||||
}
|
||||
|
||||
function reset() {
|
||||
isReasoning = false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,11 +4,11 @@ ApiStrategy {
|
||||
property bool isReasoning: false
|
||||
|
||||
function buildEndpoint(model: AiModel): string {
|
||||
console.log("[AI] Endpoint: " + model.endpoint);
|
||||
// console.log("[AI] Endpoint: " + model.endpoint);
|
||||
return model.endpoint;
|
||||
}
|
||||
|
||||
function buildRequestData(model: AiModel, messages, systemPrompt: string, temperature: real) {
|
||||
function buildRequestData(model: AiModel, messages, systemPrompt: string, temperature: real, tools: list<var>) {
|
||||
let baseData = {
|
||||
"model": model.model,
|
||||
"messages": [
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//@ pragma UseQApplication
|
||||
//@ pragma Env QS_NO_RELOAD_POPUP=1
|
||||
//@ pragma Env QT_QUICK_CONTROLS_STYLE=Basic
|
||||
//@ pragma Env QT_QUICK_FLICKABLE_WHEEL_DECELERATION=10000
|
||||
|
||||
// Adjust this to make the app smaller or larger
|
||||
//@ pragma Env QT_SCALE_FACTOR=1
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
//@ pragma UseQApplication
|
||||
//@ pragma Env QS_NO_RELOAD_POPUP=1
|
||||
//@ pragma Env QT_QUICK_CONTROLS_STYLE=Basic
|
||||
//@ pragma Env QT_QUICK_FLICKABLE_WHEEL_DECELERATION=10000
|
||||
|
||||
// Adjust this to make the shell smaller or larger
|
||||
//@ pragma Env QT_SCALE_FACTOR=1
|
||||
|
||||
|
||||
import "./modules/common/"
|
||||
import "./modules/background/"
|
||||
import "./modules/bar/"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//@ pragma UseQApplication
|
||||
//@ pragma Env QS_NO_RELOAD_POPUP=1
|
||||
//@ pragma Env QT_QUICK_CONTROLS_STYLE=Basic
|
||||
//@ pragma Env QT_QUICK_FLICKABLE_WHEEL_DECELERATION=10000
|
||||
|
||||
// Adjust this to make the app smaller or larger
|
||||
//@ pragma Env QT_SCALE_FACTOR=1
|
||||
@@ -13,6 +14,7 @@ import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Hyprland
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
|
||||
Reference in New Issue
Block a user