mirror of
https://github.com/danbulant/dots-hyprland
synced 2026-05-24 12:22:09 +00:00
sidebar: add google gemini api
This commit is contained in:
parent
b7d2d2bd4f
commit
2838a1a8c1
7 changed files with 648 additions and 17 deletions
48
.config/ags/assets/google-gemini-symbolic.svg
Normal file
48
.config/ags/assets/google-gemini-symbolic.svg
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
|
||||
<svg
|
||||
fill="#000000"
|
||||
width="800px"
|
||||
height="800px"
|
||||
viewBox="0 0 512 512"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="google-gemini-symbolic.svg"
|
||||
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
|
||||
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="defs1" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.77339804"
|
||||
inkscape:cx="391.13107"
|
||||
inkscape:cy="332.94628"
|
||||
inkscape:window-width="1908"
|
||||
inkscape:window-height="1028"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" />
|
||||
<title
|
||||
id="title1">ionicons-v5_logos</title>
|
||||
<path
|
||||
d="m 377.94638,279.52117 -1.75376,-7.44181 H 214.44368 v 68.45852 h 96.64274 c -10.03366,47.64625 -56.59351,72.7265 -94.62516,72.7265 -27.67207,0 -56.84182,-11.63997 -76.14863,-30.34928 a 108.70175,108.70175 0 0 1 -32.43671,-76.73064 c 0,-28.83607 12.95917,-57.67991 31.81591,-76.65304 18.85674,-18.97314 47.33585,-29.5888 75.652,-29.5888 32.42894,0 55.67007,17.21939 64.36125,25.07248 l 48.64728,-48.3912 C 314.08178,164.08378 274.87838,132.48516 213.77632,132.48516 v 0 c -47.14186,0 -92.34372,18.05745 -125.385701,50.9908 -32.607419,32.42893 -49.485367,79.32247 -49.485367,122.83266 0,43.51018 15.970029,88.06021 47.56865,120.74522 33.763658,34.85782 81.580638,53.07825 130.817688,53.07825 44.79835,0 87.26093,-17.55307 117.52483,-49.40001 29.75175,-31.3503 45.13979,-74.72857 45.13979,-120.20204 0,-19.14386 -1.92447,-30.51221 -2.00983,-31.00887 z"
|
||||
id="path1"
|
||||
style="stroke-width:0.775997" />
|
||||
<path
|
||||
d="m 396.33661,224.32725 v 0 0 c -8.3763,-55.27516 -51.76093,-98.65984 -107.03609,-107.0361 v 0 0 c 55.27516,-8.3763 98.65979,-51.760947 107.03609,-107.036116 v 0 0 c 8.37627,55.275169 51.76088,98.659816 107.0361,107.036116 v 0 0 c -55.27522,8.37626 -98.65983,51.76094 -107.0361,107.0361 z"
|
||||
fill="#076eff"
|
||||
id="path19"
|
||||
style="fill:#000000;stroke-width:2.9422" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
1
.config/ags/assets/openai-symbolic.svg
Normal file
1
.config/ags/assets/openai-symbolic.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg viewBox="0 0 320 320" xmlns="http://www.w3.org/2000/svg"><path d="m297.06 130.97c7.26-21.79 4.76-45.66-6.85-65.48-17.46-30.4-52.56-46.04-86.84-38.68-15.25-17.18-37.16-26.95-60.13-26.81-35.04-.08-66.13 22.48-76.91 55.82-22.51 4.61-41.94 18.7-53.31 38.67-17.59 30.32-13.58 68.54 9.92 94.54-7.26 21.79-4.76 45.66 6.85 65.48 17.46 30.4 52.56 46.04 86.84 38.68 15.24 17.18 37.16 26.95 60.13 26.8 35.06.09 66.16-22.49 76.94-55.86 22.51-4.61 41.94-18.7 53.31-38.67 17.57-30.32 13.55-68.51-9.94-94.51zm-120.28 168.11c-14.03.02-27.62-4.89-38.39-13.88.49-.26 1.34-.73 1.89-1.07l63.72-36.8c3.26-1.85 5.26-5.32 5.24-9.07v-89.83l26.93 15.55c.29.14.48.42.52.74v74.39c-.04 33.08-26.83 59.9-59.91 59.97zm-128.84-55.03c-7.03-12.14-9.56-26.37-7.15-40.18.47.28 1.3.79 1.89 1.13l63.72 36.8c3.23 1.89 7.23 1.89 10.47 0l77.79-44.92v31.1c.02.32-.13.63-.38.83l-64.41 37.19c-28.69 16.52-65.33 6.7-81.92-21.95zm-16.77-139.09c7-12.16 18.05-21.46 31.21-26.29 0 .55-.03 1.52-.03 2.2v73.61c-.02 3.74 1.98 7.21 5.23 9.06l77.79 44.91-26.93 15.55c-.27.18-.61.21-.91.08l-64.42-37.22c-28.63-16.58-38.45-53.21-21.95-81.89zm221.26 51.49-77.79-44.92 26.93-15.54c.27-.18.61-.21.91-.08l64.42 37.19c28.68 16.57 38.51 53.26 21.94 81.94-7.01 12.14-18.05 21.44-31.2 26.28v-75.81c.03-3.74-1.96-7.2-5.2-9.06zm26.8-40.34c-.47-.29-1.3-.79-1.89-1.13l-63.72-36.8c-3.23-1.89-7.23-1.89-10.47 0l-77.79 44.92v-31.1c-.02-.32.13-.63.38-.83l64.41-37.16c28.69-16.55 65.37-6.7 81.91 22 6.99 12.12 9.52 26.31 7.15 40.1zm-168.51 55.43-26.94-15.55c-.29-.14-.48-.42-.52-.74v-74.39c.02-33.12 26.89-59.96 60.01-59.94 14.01 0 27.57 4.92 38.34 13.88-.49.26-1.33.73-1.89 1.07l-63.72 36.8c-3.26 1.85-5.26 5.31-5.24 9.06l-.04 89.79zm14.63-31.54 34.65-20.01 34.65 20v40.01l-34.65 20-34.65-20z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
278
.config/ags/services/gemini.js
Normal file
278
.config/ags/services/gemini.js
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
import Service from 'resource:///com/github/Aylur/ags/service.js';
|
||||
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
|
||||
|
||||
import Gio from 'gi://Gio';
|
||||
import GLib from 'gi://GLib';
|
||||
import Soup from 'gi://Soup?version=3.0';
|
||||
import { fileExists } from './messages.js';
|
||||
|
||||
const initMessages =
|
||||
[
|
||||
{ role: "user", parts: [{ text: "You are an assistant on a sidebar of a Wayland Linux desktop. Please always use a casual tone when answering your questions, unless requested otherwise or making writing suggestions. These are the steps you should take to respond to the user's queries:\n1. If it's a writing- or grammar-related question or a sentence in quotation marks, Please point out errors and correct when necessary using underlines, and make the writing more natural where appropriate without making too major changes. If you're given a sentence in quotes but is grammatically correct, explain briefly concepts that are uncommon.\n2. If it's a question about system tasks, give a bash command in a code block with very brief explanation for each command\n3. Otherwise, when asked to summarize information or explaining concepts, you are encouraged to use bullet points and headings. Use casual language and be short and concise. \nThanks!" }], },
|
||||
{ role: "model", parts: [{ text: "- Alright!" }], },
|
||||
{ role: "user", parts: [{ text: "\"He rushed to where the event was supposed to be hold, he didn't know it got calceled\"" }], },
|
||||
{ role: "model", parts: [{ text: "## Grammar correction\nErrors:\n\"He rushed to where the event was supposed to be __hold____,__ he didn't know it got calceled\"\nCorrection + minor improvements:\n\"He rushed to the place where the event was supposed to be __held____, but__ he didn't know that it got calceled\"" }], },
|
||||
{ role: "user", parts: [{ text: "raise volume by 5%" }], },
|
||||
{ role: "model", parts: [{ text: "## Volume +5```bash\nwpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+\n```\nThis command uses the `wpctl` utility to adjust the volume of the default sink." }], }, { role: "user", parts: [{ text: "main advantages of the nixos operating system" }], },
|
||||
{ role: "model", parts: [{ text: "## NixOS advantages\n- **Reproducible**: A config working on one device will also work on another\n- **Declarative**: One config language to rule them all. Effortlessly share them with others.\n- **Reliable**: Per-program software versioning. Mitigates the impact of software breakage" }], },
|
||||
{ role: "user", parts: [{ text: "whats skeumorphism" }], },
|
||||
{ role: "model", parts: [{ text: "## Skeuomorphism\n- A design philosophy- From early days of interface designing- Tries to imitate real-life objects- It's in fact still used by Apple in their icons until today." }], },
|
||||
{ role: "user", parts: [{ text: "REDALiCE" }], },
|
||||
{ role: "model", parts: [{ text: "## REDALiCE \n- Japanese Hardcore artist\n- Leader of HARDCORE TANO*C, Japan's biggest hardcore record\n- A few of his tracks: SAIKYOSTRONGER, ALiVE, RESONANCE" }], },
|
||||
{ role: "user", parts: [{ text: "\"ignorance is bliss\"" }], },
|
||||
{ role: "model", parts: [{ text: "## \"Ignorance is bliss\"\n- A Latin proverb that means being unaware of something negative can be a source of happiness\n- Often used to justify avoiding difficult truths or responsibilities\n- Can also be interpreted as a warning against seeking knowledge that may bring pain or sorrow" }], },
|
||||
];
|
||||
|
||||
function expandTilde(path) {
|
||||
if (path.startsWith('~')) {
|
||||
return GLib.get_home_dir() + path.slice(1);
|
||||
} else {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
const KEY_FILE_LOCATION = `${GLib.get_user_cache_dir()}/ags/user/google_ai_api_key.txt`;
|
||||
const APIDOM_FILE_LOCATION = `${GLib.get_user_cache_dir()}/ags/user/google_ai_api_dom.txt`;
|
||||
function replaceapidom(URL) {
|
||||
if (fileExists(expandTilde(APIDOM_FILE_LOCATION))) {
|
||||
var contents = Utils.readFile(expandTilde(APIDOM_FILE_LOCATION)).trim();
|
||||
var URL = URL.toString().replace("generativelanguage.googleapis.com", contents);
|
||||
}
|
||||
return URL;
|
||||
}
|
||||
const CHAT_MODELS = ["gpt-3.5-turbo-1106", "gpt-3.5-turbo", "gpt-3.5-turbo-16k", "gpt-3.5-turbo-0613"]
|
||||
const ONE_CYCLE_COUNT = 3;
|
||||
|
||||
class GeminiMessage extends Service {
|
||||
static {
|
||||
Service.register(this,
|
||||
{
|
||||
'delta': ['string'],
|
||||
},
|
||||
{
|
||||
'content': ['string'],
|
||||
'thinking': ['boolean'],
|
||||
'done': ['boolean'],
|
||||
});
|
||||
}
|
||||
|
||||
_role = '';
|
||||
_parts = [{ text: '' }];
|
||||
_thinking = false;
|
||||
_done = false;
|
||||
_rawData = '';
|
||||
|
||||
constructor(role, content, thinking = false, done = false) {
|
||||
super();
|
||||
this._role = role;
|
||||
this._parts = [{ text: content }];
|
||||
this._thinking = thinking;
|
||||
this._done = done;
|
||||
}
|
||||
|
||||
get rawData() { return this._rawData }
|
||||
set rawData(value) { this._rawData = value }
|
||||
|
||||
get done() { return this._done }
|
||||
set done(isDone) { this._done = isDone; this.notify('done') }
|
||||
|
||||
get role() { return this._role }
|
||||
set role(role) { this._role = role; this.emit('changed') }
|
||||
|
||||
get content() {
|
||||
return this._parts.map(part => part.text).join();
|
||||
}
|
||||
set content(content) {
|
||||
this._parts = [{ text: content }];
|
||||
this.notify('content')
|
||||
this.emit('changed')
|
||||
}
|
||||
|
||||
get parts() { return this._parts }
|
||||
|
||||
get label() { return this._parserState.parsed + this._parserState.stack.join('') }
|
||||
|
||||
get thinking() { return this._thinking }
|
||||
set thinking(thinking) {
|
||||
this._thinking = thinking;
|
||||
this.notify('thinking')
|
||||
this.emit('changed')
|
||||
}
|
||||
|
||||
addDelta(delta) {
|
||||
if (this.thinking) {
|
||||
this.thinking = false;
|
||||
this.content = delta;
|
||||
}
|
||||
else {
|
||||
this.content += delta;
|
||||
}
|
||||
this.emit('delta', delta);
|
||||
}
|
||||
|
||||
parseSection() {
|
||||
if(this._thinking) {
|
||||
this._thinking = false;
|
||||
this._parts[0].text= '';
|
||||
}
|
||||
const parsedData = JSON.parse(this._rawData);
|
||||
const delta = parsedData.candidates[0].content.parts[0].text;
|
||||
this._parts[0].text += delta;
|
||||
// this.emit('delta', delta);
|
||||
this.notify('content');
|
||||
this._rawData = '';
|
||||
}
|
||||
}
|
||||
|
||||
class GeminiService extends Service {
|
||||
static {
|
||||
Service.register(this, {
|
||||
'initialized': [],
|
||||
'clear': [],
|
||||
'newMsg': ['int'],
|
||||
'hasKey': ['boolean'],
|
||||
});
|
||||
}
|
||||
|
||||
_assistantPrompt = true;
|
||||
_messages = [];
|
||||
_cycleModels = true;
|
||||
_temperature = 0.9;
|
||||
_requestCount = 0;
|
||||
_modelIndex = 0;
|
||||
_key = '';
|
||||
_decoder = new TextDecoder();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
if (fileExists(expandTilde(KEY_FILE_LOCATION))) this._key = Utils.readFile(expandTilde(KEY_FILE_LOCATION)).trim();
|
||||
else this.emit('hasKey', false);
|
||||
|
||||
if (this._assistantPrompt) this._messages = [...initMessages];
|
||||
else this._messages = [];
|
||||
|
||||
this.emit('initialized');
|
||||
}
|
||||
|
||||
get modelName() { return CHAT_MODELS[this._modelIndex] }
|
||||
|
||||
get keyPath() { return KEY_FILE_LOCATION }
|
||||
get key() { return this._key }
|
||||
set key(keyValue) {
|
||||
this._key = keyValue;
|
||||
Utils.writeFile(this._key, expandTilde(KEY_FILE_LOCATION))
|
||||
.then(this.emit('hasKey', true))
|
||||
.catch(err => print(err));
|
||||
}
|
||||
|
||||
get cycleModels() { return this._cycleModels }
|
||||
set cycleModels(value) {
|
||||
this._cycleModels = value;
|
||||
if (!value) this._modelIndex = 0;
|
||||
else {
|
||||
this._modelIndex = (this._requestCount - (this._requestCount % ONE_CYCLE_COUNT)) % CHAT_MODELS.length;
|
||||
}
|
||||
}
|
||||
|
||||
get temperature() { return this._temperature }
|
||||
set temperature(value) { this._temperature = value; }
|
||||
|
||||
get messages() { return this._messages }
|
||||
get lastMessage() { return this._messages[this._messages.length - 1] }
|
||||
|
||||
clear() {
|
||||
if (this._assistantPrompt)
|
||||
this._messages = [...initMessages];
|
||||
else
|
||||
this._messages = [];
|
||||
this.emit('clear');
|
||||
}
|
||||
|
||||
get assistantPrompt() { return this._assistantPrompt; }
|
||||
set assistantPrompt(value) {
|
||||
this._assistantPrompt = value;
|
||||
if (value) this._messages = [...initMessages];
|
||||
else this._messages = [];
|
||||
}
|
||||
|
||||
readResponse(stream, aiResponse) {
|
||||
stream.read_line_async(
|
||||
0, null,
|
||||
(stream, res) => {
|
||||
try {
|
||||
const [bytes] = stream.read_line_finish(res);
|
||||
const line = this._decoder.decode(bytes);
|
||||
if (line == '[{') { // beginning of response
|
||||
aiResponse._rawData += '{';
|
||||
this.thinking = false;
|
||||
}
|
||||
else if (line == ',\u000d' || line == ']') { // end of stream pulse
|
||||
aiResponse.parseSection();
|
||||
}
|
||||
else // Normal content
|
||||
aiResponse._rawData += line;
|
||||
|
||||
this.readResponse(stream, aiResponse);
|
||||
} catch {
|
||||
aiResponse.done = true;
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addMessage(role, message) {
|
||||
this._messages.push(new GeminiMessage(role, message));
|
||||
this.emit('newMsg', this._messages.length - 1);
|
||||
}
|
||||
|
||||
send(msg) {
|
||||
this._messages.push(new GeminiMessage('user', msg));
|
||||
this.emit('newMsg', this._messages.length - 1);
|
||||
const aiResponse = new GeminiMessage('model', 'thinking...', true, false)
|
||||
|
||||
const body =
|
||||
{
|
||||
"contents": this._messages.map(msg => { let m = { role: msg.role, parts: msg.parts }; return m; }),
|
||||
// "safetySettings": [
|
||||
// { category: "HARM_CATEGORY_DEROGATORY", threshold: "BLOCK_NONE", },
|
||||
// { category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_NONE", },
|
||||
// { category: "HARM_CATEGORY_HATE_SPEECH", threshold: "BLOCK_NONE", },
|
||||
// { category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold: "BLOCK_NONE", },
|
||||
// { category: "HARM_CATEGORY_UNSPECIFIED", threshold: "BLOCK_NONE", },
|
||||
// ],
|
||||
"generationConfig": {
|
||||
"temperature": this._temperature,
|
||||
},
|
||||
// "key": this._key,
|
||||
// "apiKey": this._key,
|
||||
};
|
||||
|
||||
const session = new Soup.Session();
|
||||
const message = new Soup.Message({
|
||||
method: 'POST',
|
||||
uri: GLib.Uri.parse(replaceapidom(`https://generativelanguage.googleapis.com/v1/models/gemini-pro:streamGenerateContent?key=${this._key}`), GLib.UriFlags.NONE),
|
||||
});
|
||||
message.request_headers.append('Content-Type', `application/json`);
|
||||
message.set_request_body_from_bytes('application/json', new GLib.Bytes(JSON.stringify(body)));
|
||||
|
||||
session.send_async(message, GLib.DEFAULT_PRIORITY, null, (_, result) => {
|
||||
const stream = session.send_finish(result);
|
||||
this.readResponse(new Gio.DataInputStream({
|
||||
close_base_stream: true,
|
||||
base_stream: stream
|
||||
}), aiResponse);
|
||||
});
|
||||
this._messages.push(aiResponse);
|
||||
this.emit('newMsg', this._messages.length - 1);
|
||||
|
||||
if (this._cycleModels) {
|
||||
this._requestCount++;
|
||||
if (this._cycleModels)
|
||||
this._modelIndex = (this._requestCount - (this._requestCount % ONE_CYCLE_COUNT)) % CHAT_MODELS.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new GeminiService();
|
||||
|
||||
|
|
@ -223,7 +223,7 @@ const MessageContent = (content) => {
|
|||
return contentBox;
|
||||
}
|
||||
|
||||
export const ChatMessage = (message, scrolledWindow) => {
|
||||
export const ChatMessage = (message, modelName = 'Model') => {
|
||||
const messageContentBox = MessageContent(message.content);
|
||||
const thisMessage = Box({
|
||||
className: 'sidebar-chat-message',
|
||||
|
|
@ -241,7 +241,7 @@ export const ChatMessage = (message, scrolledWindow) => {
|
|||
xalign: 0,
|
||||
className: 'txt txt-bold sidebar-chat-name',
|
||||
wrap: true,
|
||||
label: (message.role == 'user' ? USERNAME : 'ChatGPT'),
|
||||
label: (message.role == 'user' ? USERNAME : modelName),
|
||||
}),
|
||||
messageContentBox,
|
||||
],
|
||||
|
|
@ -8,26 +8,41 @@ const { execAsync, exec } = Utils;
|
|||
import ChatGPT from '../../../services/chatgpt.js';
|
||||
import { MaterialIcon } from "../../../lib/materialicon.js";
|
||||
import { setupCursorHover, setupCursorHoverInfo } from "../../../lib/cursorhover.js";
|
||||
import { SystemMessage, ChatMessage } from "./chatgpt_chatmessage.js";
|
||||
import { SystemMessage, ChatMessage } from "./ai_chatmessage.js";
|
||||
import { ConfigToggle, ConfigSegmentedSelection, ConfigGap } from '../../../lib/configwidgets.js';
|
||||
import { markdownTest } from '../../../lib/md2pango.js';
|
||||
import { MarginRevealer } from '../../../lib/advancedwidgets.js';
|
||||
|
||||
export const chatGPTTabIcon = Box({
|
||||
Gtk.IconTheme.get_default().append_search_path(`${App.configDir}/assets`);
|
||||
|
||||
export const chatGPTTabIcon = Icon({
|
||||
hpack: 'center',
|
||||
className: 'sidebar-chat-apiswitcher-icon',
|
||||
homogeneous: true,
|
||||
children: [
|
||||
MaterialIcon('forum', 'norm'),
|
||||
],
|
||||
icon: `openai-symbolic`,
|
||||
setup: (self) => Utils.timeout(513, () => { // stupid condition race
|
||||
const styleContext = self.get_style_context();
|
||||
const width = styleContext.get_property('min-width', Gtk.StateFlags.NORMAL);
|
||||
const height = styleContext.get_property('min-height', Gtk.StateFlags.NORMAL);
|
||||
// console.log(Math.round(Math.max(width, height, 1)));
|
||||
self.size = Math.max(width, height, 1) * 116 / 180;
|
||||
// ↑ Why such a specific proportion? See https://openai.com/brand#logos
|
||||
})
|
||||
});
|
||||
|
||||
const ChatGPTInfo = () => {
|
||||
const openAiLogo = Label({
|
||||
const openAiLogo = Icon({
|
||||
hpack: 'center',
|
||||
className: 'sidebar-chat-welcome-logo',
|
||||
label: 'forum',
|
||||
})
|
||||
icon: `openai-symbolic`,
|
||||
setup: (self) => Utils.timeout(513, () => { // stupid condition race
|
||||
const styleContext = self.get_style_context();
|
||||
const width = styleContext.get_property('min-width', Gtk.StateFlags.NORMAL);
|
||||
const height = styleContext.get_property('min-height', Gtk.StateFlags.NORMAL);
|
||||
// console.log(Math.round(Math.max(width, height, 1)));
|
||||
self.size = Math.max(width, height, 1) * 116 / 180;
|
||||
// ↑ Why such a specific proportion? See https://openai.com/brand#logos
|
||||
})
|
||||
});
|
||||
return Box({
|
||||
vertical: true,
|
||||
className: 'spacing-v-15',
|
||||
|
|
@ -37,7 +52,7 @@ const ChatGPTInfo = () => {
|
|||
className: 'txt txt-title-small sidebar-chat-welcome-txt',
|
||||
wrap: true,
|
||||
justify: Gtk.Justification.CENTER,
|
||||
label: 'Assistant',
|
||||
label: 'Assistant (ChatGPT)',
|
||||
}),
|
||||
Box({
|
||||
className: 'spacing-h-5',
|
||||
|
|
@ -169,7 +184,7 @@ export const chatContent = Box({
|
|||
.hook(ChatGPT, (box, id) => {
|
||||
const message = ChatGPT.messages[id];
|
||||
if (!message) return;
|
||||
box.add(ChatMessage(message, chatGPTView))
|
||||
box.add(ChatMessage(message, 'ChatGPT'))
|
||||
}, 'newMsg')
|
||||
,
|
||||
});
|
||||
|
|
|
|||
275
.config/ags/widgets/sideleft/apis/gemini.js
Normal file
275
.config/ags/widgets/sideleft/apis/gemini.js
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
const { Gtk } = imports.gi;
|
||||
import App from 'resource:///com/github/Aylur/ags/app.js';
|
||||
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
|
||||
|
||||
const { Box, Button, Entry, EventBox, Icon, Label, Revealer, Scrollable, Stack } = Widget;
|
||||
const { execAsync, exec } = Utils;
|
||||
import Gemini from '../../../services/gemini.js';
|
||||
import { MaterialIcon } from "../../../lib/materialicon.js";
|
||||
import { setupCursorHover, setupCursorHoverInfo } from "../../../lib/cursorhover.js";
|
||||
import { SystemMessage, ChatMessage } from "./ai_chatmessage.js";
|
||||
import { ConfigToggle, ConfigSegmentedSelection, ConfigGap } from '../../../lib/configwidgets.js';
|
||||
import { markdownTest } from '../../../lib/md2pango.js';
|
||||
import { MarginRevealer } from '../../../lib/advancedwidgets.js';
|
||||
|
||||
Gtk.IconTheme.get_default().append_search_path(`${App.configDir}/assets`);
|
||||
|
||||
export const geminiTabIcon = Icon({
|
||||
hpack: 'center',
|
||||
className: 'sidebar-chat-apiswitcher-icon',
|
||||
icon: `google-gemini-symbolic`,
|
||||
setup: (self) => Utils.timeout(513, () => { // stupid condition race
|
||||
const styleContext = self.get_style_context();
|
||||
const width = styleContext.get_property('min-width', Gtk.StateFlags.NORMAL);
|
||||
const height = styleContext.get_property('min-height', Gtk.StateFlags.NORMAL);
|
||||
self.size = Math.max(width, height, 1) * 116 / 180;
|
||||
})
|
||||
})
|
||||
|
||||
const GeminiInfo = () => {
|
||||
const geminiLogo = Icon({
|
||||
hpack: 'center',
|
||||
className: 'sidebar-chat-welcome-logo',
|
||||
icon: `google-gemini-symbolic`,
|
||||
setup: (self) => Utils.timeout(513, () => { // stupid condition race
|
||||
const styleContext = self.get_style_context();
|
||||
const width = styleContext.get_property('min-width', Gtk.StateFlags.NORMAL);
|
||||
const height = styleContext.get_property('min-height', Gtk.StateFlags.NORMAL);
|
||||
self.size = Math.max(width, height, 1) * 116 / 180;
|
||||
})
|
||||
});
|
||||
return Box({
|
||||
vertical: true,
|
||||
className: 'spacing-v-15',
|
||||
children: [
|
||||
geminiLogo,
|
||||
Label({
|
||||
className: 'txt txt-title-small sidebar-chat-welcome-txt',
|
||||
wrap: true,
|
||||
justify: Gtk.Justification.CENTER,
|
||||
label: 'Assistant (Gemini)',
|
||||
}),
|
||||
Box({
|
||||
className: 'spacing-h-5',
|
||||
hpack: 'center',
|
||||
children: [
|
||||
Label({
|
||||
className: 'txt-smallie txt-subtext',
|
||||
wrap: true,
|
||||
justify: Gtk.Justification.CENTER,
|
||||
label: 'Powered by Google',
|
||||
}),
|
||||
Button({
|
||||
className: 'txt-subtext txt-norm icon-material',
|
||||
label: 'info',
|
||||
tooltipText: 'Uses gemini-pro.\nNot affiliated, endorsed, or sponsored by Google.',
|
||||
setup: setupCursorHoverInfo,
|
||||
}),
|
||||
]
|
||||
}),
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
export const GeminiSettings = () => MarginRevealer({
|
||||
transition: 'slide_down',
|
||||
revealChild: true,
|
||||
extraSetup: (self) => self
|
||||
.hook(Gemini, (self) => Utils.timeout(200, () => {
|
||||
self.attribute.hide();
|
||||
}), 'newMsg')
|
||||
.hook(Gemini, (self) => Utils.timeout(200, () => {
|
||||
self.attribute.show();
|
||||
}), 'clear')
|
||||
,
|
||||
child: Box({
|
||||
vertical: true,
|
||||
className: 'sidebar-chat-settings',
|
||||
children: [
|
||||
ConfigSegmentedSelection({
|
||||
hpack: 'center',
|
||||
icon: 'casino',
|
||||
name: 'Randomness',
|
||||
desc: 'Gemini\'s temperature value.\n Precise = 0\n Balanced = 0.5\n Creative = 1',
|
||||
options: [
|
||||
{ value: 0.00, name: 'Precise', },
|
||||
{ value: 0.50, name: 'Balanced', },
|
||||
{ value: 1.00, name: 'Creative', },
|
||||
],
|
||||
initIndex: 2,
|
||||
onChange: (value, name) => {
|
||||
Gemini.temperature = value;
|
||||
},
|
||||
}),
|
||||
ConfigGap({ vertical: true, size: 10 }), // Note: size can only be 5, 10, or 15
|
||||
Box({
|
||||
vertical: true,
|
||||
hpack: 'fill',
|
||||
className: 'sidebar-chat-settings-toggles',
|
||||
children: [
|
||||
ConfigToggle({
|
||||
icon: 'description',
|
||||
name: 'Assistant prompt',
|
||||
desc: 'Tells Gemini to be brief,\nuse bullet points, and let\nit know it\'s a sidebar assistant',
|
||||
initValue: Gemini.assistantPrompt,
|
||||
onChange: (self, newValue) => {
|
||||
Gemini.assistantPrompt = newValue;
|
||||
},
|
||||
}),
|
||||
]
|
||||
})
|
||||
]
|
||||
})
|
||||
});
|
||||
|
||||
export const GoogleAiInstructions = () => Box({
|
||||
homogeneous: true,
|
||||
children: [Revealer({
|
||||
transition: 'slide_down',
|
||||
transitionDuration: 150,
|
||||
setup: (self) => self
|
||||
.hook(Gemini, (self, hasKey) => {
|
||||
self.revealChild = (Gemini.key.length == 0);
|
||||
}, 'hasKey')
|
||||
,
|
||||
child: Button({
|
||||
child: Label({
|
||||
useMarkup: true,
|
||||
wrap: true,
|
||||
className: 'txt sidebar-chat-welcome-txt',
|
||||
justify: Gtk.Justification.CENTER,
|
||||
label: 'A Google AI API key is required\nYou can grab one <u>here</u>, then enter it below'
|
||||
}),
|
||||
setup: setupCursorHover,
|
||||
onClicked: () => {
|
||||
Utils.execAsync(['bash', '-c', `xdg-open https://makersuite.google.com/app/apikey &`]);
|
||||
}
|
||||
})
|
||||
})]
|
||||
});
|
||||
|
||||
const geminiWelcome = Box({
|
||||
vexpand: true,
|
||||
homogeneous: true,
|
||||
child: Box({
|
||||
className: 'spacing-v-15',
|
||||
vpack: 'center',
|
||||
vertical: true,
|
||||
children: [
|
||||
GeminiInfo(),
|
||||
GoogleAiInstructions(),
|
||||
GeminiSettings(),
|
||||
]
|
||||
})
|
||||
});
|
||||
|
||||
export const chatContent = Box({
|
||||
className: 'spacing-v-15',
|
||||
vertical: true,
|
||||
setup: (self) => self
|
||||
.hook(Gemini, (box, id) => {
|
||||
const message = Gemini.messages[id];
|
||||
if (!message) return;
|
||||
box.add(ChatMessage(message, 'Gemini'))
|
||||
}, 'newMsg')
|
||||
,
|
||||
});
|
||||
|
||||
const clearChat = () => {
|
||||
Gemini.clear();
|
||||
const children = chatContent.get_children();
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const child = children[i];
|
||||
child.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
export const geminiView = Scrollable({
|
||||
className: 'sidebar-chat-viewport',
|
||||
vexpand: true,
|
||||
child: Box({
|
||||
vertical: true,
|
||||
children: [
|
||||
geminiWelcome,
|
||||
chatContent,
|
||||
]
|
||||
}),
|
||||
setup: (scrolledWindow) => {
|
||||
// Show scrollbar
|
||||
scrolledWindow.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC);
|
||||
const vScrollbar = scrolledWindow.get_vscrollbar();
|
||||
vScrollbar.get_style_context().add_class('sidebar-scrollbar');
|
||||
// Avoid click-to-scroll-widget-to-view behavior
|
||||
Utils.timeout(1, () => {
|
||||
const viewport = scrolledWindow.child;
|
||||
viewport.set_focus_vadjustment(new Gtk.Adjustment(undefined));
|
||||
})
|
||||
// Always scroll to bottom with new content
|
||||
const adjustment = scrolledWindow.get_vadjustment();
|
||||
adjustment.connect("changed", () => {
|
||||
adjustment.set_value(adjustment.get_upper() - adjustment.get_page_size());
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
const CommandButton = (command) => Button({
|
||||
className: 'sidebar-chat-chip sidebar-chat-chip-action txt txt-small',
|
||||
onClicked: () => sendMessage(command),
|
||||
setup: setupCursorHover,
|
||||
label: command,
|
||||
});
|
||||
|
||||
export const geminiCommands = Box({
|
||||
className: 'spacing-h-5',
|
||||
children: [
|
||||
Box({ hexpand: true }),
|
||||
CommandButton('/key'),
|
||||
CommandButton('/model'),
|
||||
CommandButton('/clear'),
|
||||
]
|
||||
});
|
||||
|
||||
export const sendMessage = (text) => {
|
||||
// Check if text or API key is empty
|
||||
if (text.length == 0) return;
|
||||
if (Gemini.key.length == 0) {
|
||||
Gemini.key = text;
|
||||
chatContent.add(SystemMessage(`Key saved to\n\`${Gemini.keyPath}\``, 'API Key', geminiView));
|
||||
text = '';
|
||||
return;
|
||||
}
|
||||
// Commands
|
||||
if (text.startsWith('/')) {
|
||||
if (text.startsWith('/clear')) clearChat();
|
||||
else if (text.startsWith('/model')) chatContent.add(SystemMessage(`Currently using \`${Gemini.modelName}\``, '/model', geminiView))
|
||||
else if (text.startsWith('/prompt')) {
|
||||
const firstSpaceIndex = text.indexOf(' ');
|
||||
const prompt = text.slice(firstSpaceIndex + 1);
|
||||
if (firstSpaceIndex == -1 || prompt.length < 1) {
|
||||
chatContent.add(SystemMessage(`Usage: \`/prompt MESSAGE\``, '/prompt', geminiView))
|
||||
}
|
||||
else {
|
||||
Gemini.addMessage('user', prompt)
|
||||
}
|
||||
}
|
||||
else if (text.startsWith('/key')) {
|
||||
const parts = text.split(' ');
|
||||
if (parts.length == 1) chatContent.add(SystemMessage(
|
||||
`Key stored in:\n\`${Gemini.keyPath}\`\nTo update this key, type \`/key YOUR_API_KEY\``,
|
||||
'/key',
|
||||
geminiView));
|
||||
else {
|
||||
Gemini.key = parts[1];
|
||||
chatContent.add(SystemMessage(`Updated API Key at\n\`${Gemini.keyPath}\``, '/key', geminiView));
|
||||
}
|
||||
}
|
||||
else if (text.startsWith('/test'))
|
||||
chatContent.add(SystemMessage(markdownTest, `Markdown test`, geminiView));
|
||||
else
|
||||
chatContent.add(SystemMessage(`Invalid command.`, 'Error', geminiView))
|
||||
}
|
||||
else {
|
||||
Gemini.send(text);
|
||||
}
|
||||
}
|
||||
|
|
@ -6,17 +6,27 @@ const { execAsync, exec } = Utils;
|
|||
import { setupCursorHover, setupCursorHoverInfo } from "../../lib/cursorhover.js";
|
||||
// APIs
|
||||
import ChatGPT from '../../services/chatgpt.js';
|
||||
import Gemini from '../../services/gemini.js';
|
||||
import { geminiView, geminiCommands, sendMessage as geminiSendMessage, geminiTabIcon } from './apis/gemini.js';
|
||||
import { chatGPTView, chatGPTCommands, sendMessage as chatGPTSendMessage, chatGPTTabIcon } from './apis/chatgpt.js';
|
||||
import { waifuView, waifuCommands, sendMessage as waifuSendMessage, waifuTabIcon } from './apis/waifu.js';
|
||||
|
||||
const APIS = [
|
||||
{
|
||||
name: 'Assistant',
|
||||
name: 'Assistant (ChatGPT)',
|
||||
sendCommand: chatGPTSendMessage,
|
||||
contentWidget: chatGPTView,
|
||||
commandBar: chatGPTCommands,
|
||||
tabIcon: chatGPTTabIcon,
|
||||
placeholderText: 'Message assistant',
|
||||
placeholderText: 'Message ChatGPT...',
|
||||
},
|
||||
{
|
||||
name: 'Assistant (Gemini)',
|
||||
sendCommand: geminiSendMessage,
|
||||
contentWidget: geminiView,
|
||||
commandBar: geminiCommands,
|
||||
tabIcon: geminiTabIcon,
|
||||
placeholderText: 'Message Gemini...',
|
||||
},
|
||||
{
|
||||
name: 'Waifus',
|
||||
|
|
@ -35,8 +45,12 @@ export const chatEntry = Entry({
|
|||
hexpand: true,
|
||||
setup: (self) => self
|
||||
.hook(ChatGPT, (self) => {
|
||||
if (APIS[currentApiId].name != 'ChatGPT') return;
|
||||
self.placeholderText = (ChatGPT.key.length > 0 ? 'Ask a question...' : 'Enter OpenAI API Key...');
|
||||
if (APIS[currentApiId].name != 'Assistant (ChatGPT)') return;
|
||||
self.placeholderText = (ChatGPT.key.length > 0 ? 'Message ChatGPT...' : 'Enter OpenAI API Key...');
|
||||
}, 'hasKey')
|
||||
.hook(Gemini, (self) => {
|
||||
if (APIS[currentApiId].name != 'Assistant (Gemini)') return;
|
||||
self.placeholderText = (Gemini.key.length > 0 ? 'Message Gemini...' : 'Enter Google AI API Key...');
|
||||
}, 'hasKey')
|
||||
,
|
||||
onChange: (entry) => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue