forked from Shinonome/dots-hyprland
292 lines
11 KiB
JavaScript
292 lines
11 KiB
JavaScript
const { Gdk, Gio, GLib, Gtk, Pango } = imports.gi;
|
|
import { App, Utils, Widget } from '../../../imports.js';
|
|
const { Box, Button, Entry, EventBox, Icon, Label, Revealer, Scrollable, Stack } = Widget;
|
|
const { execAsync, exec } = Utils;
|
|
import { MaterialIcon } from "../../../lib/materialicon.js";
|
|
import { convert } from "../../../lib/md2pango.js";
|
|
import GtkSource from "gi://GtkSource?version=3.0";
|
|
|
|
const CUSTOM_SOURCEVIEW_SCHEME_PATH = `${App.configDir}/data/sourceviewtheme.xml`;
|
|
const CUSTOM_SCHEME_ID = 'custom';
|
|
const USERNAME = GLib.get_user_name();
|
|
const CHATGPT_CURSOR = ' >> ';
|
|
const MESSAGE_SCROLL_DELAY = 13; // In milliseconds, the time before an updated message scrolls to bottom
|
|
|
|
/////////////////////// Custom source view colorscheme /////////////////////////
|
|
|
|
function loadCustomColorScheme(filePath) {
|
|
// Read the XML file content
|
|
const file = Gio.File.new_for_path(filePath);
|
|
const [success, contents] = file.load_contents(null);
|
|
|
|
if (!success) {
|
|
logError('Failed to load the XML file.');
|
|
return;
|
|
}
|
|
|
|
// Parse the XML content and set the Style Scheme
|
|
const schemeManager = GtkSource.StyleSchemeManager.get_default();
|
|
schemeManager.append_search_path(file.get_parent().get_path());
|
|
}
|
|
loadCustomColorScheme(CUSTOM_SOURCEVIEW_SCHEME_PATH);
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
|
|
function copyToClipboard(text) {
|
|
const clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD);
|
|
const textVariant = new GLib.Variant('s', text);
|
|
clipboard.set_text(textVariant, -1);
|
|
clipboard.store();
|
|
}
|
|
|
|
function substituteLang(str) {
|
|
const subs = [
|
|
{ from: 'javascript', to: 'js' },
|
|
];
|
|
|
|
for (const { from, to } of subs) {
|
|
if (from === str)
|
|
return to;
|
|
}
|
|
|
|
return str;
|
|
}
|
|
|
|
const HighlightedCode = (content, lang) => {
|
|
const buffer = new GtkSource.Buffer();
|
|
const sourceView = new GtkSource.View({
|
|
buffer: buffer,
|
|
wrap_mode: Gtk.WrapMode.WORD
|
|
});
|
|
const langManager = GtkSource.LanguageManager.get_default();
|
|
let displayLang = langManager.get_language(substituteLang(lang)); // Set your preferred language
|
|
if (displayLang) {
|
|
buffer.set_language(displayLang);
|
|
}
|
|
const schemeManager = GtkSource.StyleSchemeManager.get_default();
|
|
buffer.set_style_scheme(schemeManager.get_scheme(CUSTOM_SCHEME_ID));
|
|
buffer.set_text(content, -1);
|
|
return sourceView;
|
|
}
|
|
|
|
const TextBlock = (content = '') => Label({
|
|
hpack: 'fill',
|
|
className: 'txt sidebar-chat-txtblock sidebar-chat-txt',
|
|
useMarkup: true,
|
|
xalign: 0,
|
|
wrap: true,
|
|
selectable: true,
|
|
label: content,
|
|
});
|
|
|
|
const CodeBlock = (content = '', lang = 'txt') => {
|
|
const topBar = Box({
|
|
className: 'sidebar-chat-codeblock-topbar',
|
|
children: [
|
|
Label({
|
|
label: lang,
|
|
className: 'sidebar-chat-codeblock-topbar-txt',
|
|
}),
|
|
Box({
|
|
hexpand: true,
|
|
}),
|
|
Button({
|
|
className: 'sidebar-chat-codeblock-topbar-btn',
|
|
onClicked: (self) => {
|
|
// execAsync(['bash', '-c', `wl-copy '${content}'`, `&`]).catch(print);
|
|
execAsync([`wl-copy`, `${sourceView.label}`]).catch(print);
|
|
},
|
|
child: Box({
|
|
className: 'spacing-h-5',
|
|
children: [
|
|
MaterialIcon('content_copy', 'small'),
|
|
Label({
|
|
label: 'Copy',
|
|
})
|
|
]
|
|
})
|
|
})
|
|
]
|
|
})
|
|
// Source view
|
|
const sourceView = HighlightedCode(content, lang);
|
|
|
|
const codeBlock = Box({
|
|
properties: [
|
|
['updateText', (text) => {
|
|
sourceView.get_buffer().set_text(text, -1);
|
|
}]
|
|
],
|
|
className: 'sidebar-chat-codeblock',
|
|
vertical: true,
|
|
children: [
|
|
topBar,
|
|
Box({
|
|
className: 'sidebar-chat-codeblock-code',
|
|
homogeneous: true,
|
|
children: [sourceView,],
|
|
})
|
|
]
|
|
})
|
|
|
|
// const schemeIds = styleManager.get_scheme_ids();
|
|
|
|
// print("Available Style Schemes:");
|
|
// for (let i = 0; i < schemeIds.length; i++) {
|
|
// print(schemeIds[i]);
|
|
// }
|
|
return codeBlock;
|
|
}
|
|
|
|
const Divider = () => Box({
|
|
className: 'sidebar-chat-divider',
|
|
})
|
|
|
|
const MessageContent = (content) => {
|
|
const contentBox = Box({
|
|
vertical: true,
|
|
properties: [
|
|
['fullUpdate', (self, content, useCursor = false) => {
|
|
// Clear and add first text widget
|
|
contentBox.get_children().forEach(ch => ch.destroy());
|
|
contentBox.add(TextBlock())
|
|
// Loop lines. Put normal text in markdown parser
|
|
// and put code into code highlighter (TODO)
|
|
let lines = content.split('\n');
|
|
let lastProcessed = 0;
|
|
let inCode = false;
|
|
for (const [index, line] of lines.entries()) {
|
|
// Code blocks
|
|
const codeBlockRegex = /^\s*```([a-zA-Z0-9]+)?\n?/;
|
|
if (codeBlockRegex.test(line)) {
|
|
// console.log(`code at line ${index}`);
|
|
const kids = self.get_children();
|
|
const lastLabel = kids[kids.length - 1];
|
|
const blockContent = lines.slice(lastProcessed, index).join('\n');
|
|
if (!inCode) {
|
|
lastLabel.label = convert(blockContent);
|
|
contentBox.add(CodeBlock('', codeBlockRegex.exec(line)[1]));
|
|
}
|
|
else {
|
|
lastLabel._updateText(blockContent);
|
|
contentBox.add(TextBlock());
|
|
}
|
|
|
|
lastProcessed = index + 1;
|
|
inCode = !inCode;
|
|
}
|
|
// Breaks
|
|
const dividerRegex = /^\s*---/;
|
|
if (!inCode && dividerRegex.test(line)) {
|
|
const kids = self.get_children();
|
|
const lastLabel = kids[kids.length - 1];
|
|
const blockContent = lines.slice(lastProcessed, index).join('\n');
|
|
lastLabel.label = convert(blockContent);
|
|
contentBox.add(Divider());
|
|
contentBox.add(TextBlock());
|
|
lastProcessed = index + 1;
|
|
}
|
|
}
|
|
if (lastProcessed < lines.length) {
|
|
const kids = self.get_children();
|
|
const lastLabel = kids[kids.length - 1];
|
|
let blockContent = lines.slice(lastProcessed, lines.length).join('\n');
|
|
if (!inCode)
|
|
lastLabel.label = `${convert(blockContent)}${useCursor ? CHATGPT_CURSOR : ''}`;
|
|
else
|
|
lastLabel._updateText(blockContent);
|
|
}
|
|
// Debug: plain text
|
|
// contentBox.add(Label({
|
|
// hpack: 'fill',
|
|
// className: 'txt sidebar-chat-txtblock sidebar-chat-txt',
|
|
// useMarkup: false,
|
|
// xalign: 0,
|
|
// wrap: true,
|
|
// selectable: true,
|
|
// label: '------------------------------\n' + convert(content),
|
|
// }))
|
|
contentBox.show_all();
|
|
}]
|
|
]
|
|
});
|
|
contentBox._fullUpdate(contentBox, content, false);
|
|
return contentBox;
|
|
}
|
|
|
|
export const ChatMessage = (message) => {
|
|
const messageContentBox = MessageContent(message.content);
|
|
const thisMessage = Box({
|
|
className: 'sidebar-chat-message',
|
|
children: [
|
|
Box({
|
|
className: `sidebar-chat-indicator ${message.role == 'user' ? 'sidebar-chat-indicator-user' : 'sidebar-chat-indicator-bot'}`,
|
|
}),
|
|
Box({
|
|
vertical: true,
|
|
hpack: 'fill',
|
|
hexpand: true,
|
|
children: [
|
|
Label({
|
|
hpack: 'fill',
|
|
xalign: 0,
|
|
className: 'txt txt-bold sidebar-chat-name',
|
|
wrap: true,
|
|
label: (message.role == 'user' ? USERNAME : 'ChatGPT'),
|
|
}),
|
|
messageContentBox,
|
|
],
|
|
connections: [
|
|
[message, (self, isThinking) => {
|
|
messageContentBox.toggleClassName('thinking', message.thinking);
|
|
}, 'notify::thinking'],
|
|
[message, (self) => { // Message update
|
|
messageContentBox._fullUpdate(messageContentBox, message.content, message.role != 'user');
|
|
Utils.timeout(MESSAGE_SCROLL_DELAY, () => {
|
|
const scrolledWindow = thisMessage.get_parent().get_parent();
|
|
var adjustment = scrolledWindow.get_vadjustment();
|
|
adjustment.set_value(adjustment.get_upper() - adjustment.get_page_size());
|
|
});
|
|
}, 'notify::content'],
|
|
[message, (label, isDone) => { // Remove the cursor
|
|
messageContentBox._fullUpdate(messageContentBox, message.content, false);
|
|
}, 'notify::done'],
|
|
]
|
|
})
|
|
]
|
|
});
|
|
return thisMessage;
|
|
}
|
|
|
|
export const SystemMessage = (content, commandName) => {
|
|
const messageContentBox = MessageContent(content);
|
|
const thisMessage = Box({
|
|
className: 'sidebar-chat-message',
|
|
children: [
|
|
Box({
|
|
className: `sidebar-chat-indicator sidebar-chat-indicator-System`,
|
|
}),
|
|
Box({
|
|
vertical: true,
|
|
hpack: 'fill',
|
|
hexpand: true,
|
|
children: [
|
|
Label({
|
|
xalign: 0,
|
|
className: 'txt txt-bold sidebar-chat-name',
|
|
wrap: true,
|
|
label: `System • ${commandName}`,
|
|
}),
|
|
messageContentBox,
|
|
],
|
|
})
|
|
],
|
|
setup: (self) => Utils.timeout(MESSAGE_SCROLL_DELAY, () => {
|
|
const scrolledWindow = thisMessage.get_parent().get_parent();
|
|
var adjustment = scrolledWindow.get_vadjustment();
|
|
adjustment.set_value(adjustment.get_upper() - adjustment.get_page_size());
|
|
})
|
|
});
|
|
return thisMessage;
|
|
}
|