sidebar: add google gemini api

This commit is contained in:
end-4
2024-01-24 20:19:00 +07:00
parent b7d2d2bd4f
commit 2838a1a8c1
7 changed files with 648 additions and 17 deletions
@@ -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,
],
+26 -11
View File
@@ -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
View 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);
}
}
+18 -4
View File
@@ -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) => {