diff --git a/.config/ags/modules/.configuration/user_options.js b/.config/ags/modules/.configuration/user_options.js index 9bdef7152..bd244f5b6 100644 --- a/.config/ags/modules/.configuration/user_options.js +++ b/.config/ags/modules/.configuration/user_options.js @@ -10,7 +10,7 @@ let configOptions = { 'enhancements': true, 'useHistory': true, 'writingCursor': " ...", // Warning: Using weird characters can mess up Markdown rendering - 'proxyUrl': '', // Can be "socks5://127.0.0.1:9050" or "http://127.0.0.1:8080" for example. Leave it blank if you don't need it. + 'proxyUrl': null, // Can be "socks5://127.0.0.1:9050" or "http://127.0.0.1:8080" for example. Leave it blank if you don't need it. }, 'animations': { 'choreographyDelay': 35, diff --git a/.config/ags/modules/sideleft/apis/ai_chatmessage.js b/.config/ags/modules/sideleft/apis/ai_chatmessage.js index f9cfb04ea..df8f84c97 100644 --- a/.config/ags/modules/sideleft/apis/ai_chatmessage.js +++ b/.config/ags/modules/sideleft/apis/ai_chatmessage.js @@ -3,7 +3,7 @@ import GtkSource from "gi://GtkSource?version=3.0"; 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, Label, Icon, Scrollable } = Widget; +const { Box, Button, Label, Icon, Scrollable, Stack } = Widget; const { execAsync, exec } = Utils; import { MaterialIcon } from '../../.commonwidgets/materialicon.js'; import md2pango from '../../.miscutils/md2pango.js'; @@ -77,7 +77,7 @@ const TextBlock = (content = '') => Label({ Utils.execAsync(['bash', '-c', `rm ${LATEX_DIR}/*`]) .then(() => Utils.execAsync(['bash', '-c', `mkdir -p ${LATEX_DIR}`])) - .catch(() => { }); + .catch(() => { }); const Latex = (content = '') => { const latexViewArea = Box({ // vscroll: 'never', @@ -286,7 +286,25 @@ const MessageContent = (content) => { } export const ChatMessage = (message, modelName = 'Model') => { + const TextSkeleton = (extraClassName = '') => Box({ + className: `sidebar-chat-message-skeletonline ${extraClassName}`, + }) const messageContentBox = MessageContent(message.content); + const messageLoadingSkeleton = Box({ + vertical: true, + className: 'spacing-v-5', + children: Array.from({ length: 3 }, (_, id) => TextSkeleton(`sidebar-chat-message-skeletonline-offset${id}`)), + }) + const messageArea = Stack({ + homogeneous: message.role !== 'user', + transition: 'crossfade', + transitionDuration: userOptions.animations.durationLarge, + children: { + 'thinking': messageLoadingSkeleton, + 'message': messageContentBox, + }, + shown: message.thinking ? 'thinking' : 'message', + }); const thisMessage = Box({ className: 'sidebar-chat-message', homogeneous: true, @@ -302,11 +320,15 @@ export const ChatMessage = (message, modelName = 'Model') => { useMarkup: true, label: (message.role == 'user' ? USERNAME : modelName), }), - messageContentBox, + Box({ + homogeneous: true, + className: 'sidebar-chat-messagearea', + children: [messageArea] + }) ], setup: (self) => self .hook(message, (self, isThinking) => { - messageContentBox.toggleClassName('thinking', message.thinking); + messageArea.shown = message.thinking ? 'thinking' : 'message'; }, 'notify::thinking') .hook(message, (self) => { // Message update messageContentBox.attribute.fullUpdate(messageContentBox, message.content, message.role != 'user'); diff --git a/.config/ags/scss/_lib_mixins.scss b/.config/ags/scss/_lib_mixins.scss index 6e2efdb6a..5ab167acf 100644 --- a/.config/ags/scss/_lib_mixins.scss +++ b/.config/ags/scss/_lib_mixins.scss @@ -98,28 +98,6 @@ $elevation_margin: 0.476rem; margin: $elevation_margin; } -@keyframes flyin-top { - from { - margin-top: -2.795rem; - } - - to { - margin-top: 0rem; - } -} - -@keyframes flyin-bottom { - from { - margin-top: 4.841rem; - margin-bottom: -4.841rem; - } - - to { - margin-bottom: 0rem; - margin-top: 0rem; - } -} - @mixin menu_decel { transition: 300ms cubic-bezier(0.1, 1, 0, 1); } @@ -163,12 +141,15 @@ $elevation_margin: 0.476rem; @mixin element_decel { transition: 300ms cubic-bezier(0, 0.55, 0.45, 1); } + @mixin element_bounceOut { transition: transform 200ms cubic-bezier(0.34, 1.56, 0.64, 1); } + @mixin element_accel { transition: 300ms cubic-bezier(0.55, 0, 1, 0.45); } + @mixin element_easeInOut { transition: 300ms cubic-bezier(0.85, 0, 0.15, 1); } diff --git a/.config/ags/scss/_sidebars.scss b/.config/ags/scss/_sidebars.scss index df2080cc2..107912812 100644 --- a/.config/ags/scss/_sidebars.scss +++ b/.config/ags/scss/_sidebars.scss @@ -539,13 +539,59 @@ $colorpicker_rounding: 0.341rem; background-color: mix($primary, $hovercolor, 40%); } +.sidebar-chat-messagearea { + margin: 0.341rem; +} + .sidebar-chat-message { - margin: 0.682rem; @include normal-rounding; @include group-padding; background-color: $layer1; } +$skeleton-accent: mix($secondary, $onSecondary, 50%); + +@keyframes sidebar-chat-message-skeletonline-anim { + 0% { + background-position: 175% 0%; + } + + 100% { + background-position: 50% 0%; + } +} + +.sidebar-chat-message-skeletonline { + border-radius: $rounding_verysmall; + min-height: 1.364rem; + background-color: $layer2; +} + +.sidebar-chat-message-skeletonline-offset0 { + background-repeat: no-repeat; + background: linear-gradient(to right, $layer3 0%, $skeleton-accent 25%, $layer3 50%, $layer3 100%); + background-size: 500% 500%; + animation: sidebar-chat-message-skeletonline-anim 3s linear; + animation-iteration-count: infinite; +} + +.sidebar-chat-message-skeletonline-offset1 { + background-repeat: no-repeat; + background: linear-gradient(to right, $layer3 0%, $layer3 50%, $skeleton-accent 75%, $layer3 100%); + background-size: 500% 500%; + animation: sidebar-chat-message-skeletonline-anim 3s linear; + animation-iteration-count: infinite; +} + +.sidebar-chat-message-skeletonline-offset2 { + margin-right: 5.795rem; + background-repeat: no-repeat; + background: linear-gradient(to right, $layer3 0%, $layer3 25%, $skeleton-accent 50%, $layer3 75%, $layer3 100%); + background-size: 500% 500%; + animation: sidebar-chat-message-skeletonline-anim 3s linear; + animation-iteration-count: infinite; +} + .sidebar-chat-indicator { @include element_decel; @include full-rounding; diff --git a/.config/ags/services/gemini.js b/.config/ags/services/gemini.js index 8aa28befa..ed373a8f6 100644 --- a/.config/ags/services/gemini.js +++ b/.config/ags/services/gemini.js @@ -62,11 +62,11 @@ class GeminiMessage extends Service { _role = ''; _parts = [{ text: '' }]; - _thinking = false; + _thinking; _done = false; _rawData = ''; - constructor(role, content, thinking = false, done = false) { + constructor(role, content, thinking = true, done = false) { super(); this._role = role; this._parts = [{ text: content }]; @@ -97,8 +97,8 @@ class GeminiMessage extends Service { get label() { return this._parserState.parsed + this._parserState.stack.join('') } get thinking() { return this._thinking } - set thinking(thinking) { - this._thinking = thinking; + set thinking(value) { + this._thinking = value; this.notify('thinking') this.emit('changed') } @@ -116,7 +116,7 @@ class GeminiMessage extends Service { parseSection() { if (this._thinking) { - this._thinking = false; + this.thinking = false; this._parts[0].text = ''; } const parsedData = JSON.parse(this._rawData); @@ -273,12 +273,12 @@ class GeminiService extends Service { } addMessage(role, message) { - this._messages.push(new GeminiMessage(role, message)); + this._messages.push(new GeminiMessage(role, message, false)); this.emit('newMsg', this._messages.length - 1); } send(msg) { - this._messages.push(new GeminiMessage('user', msg)); + this._messages.push(new GeminiMessage('user', msg, false)); this.emit('newMsg', this._messages.length - 1); const aiResponse = new GeminiMessage('model', 'thinking...', true, false) diff --git a/.config/ags/services/gpt.js b/.config/ags/services/gpt.js index c42524d76..b19d46702 100644 --- a/.config/ags/services/gpt.js +++ b/.config/ags/services/gpt.js @@ -76,10 +76,10 @@ class GPTMessage extends Service { _role = ''; _content = ''; - _thinking = false; + _thinking; _done = false; - constructor(role, content, thinking = false, done = false) { + constructor(role, content, thinking = true, done = false) { super(); this._role = role; this._content = content; @@ -103,8 +103,8 @@ class GPTMessage extends Service { get label() { return this._parserState.parsed + this._parserState.stack.join('') } get thinking() { return this._thinking } - set thinking(thinking) { - this._thinking = thinking; + set thinking(value) { + this._thinking = value; this.notify('thinking') this.emit('changed') } @@ -202,6 +202,7 @@ class GPTService extends Service { } readResponse(stream, aiResponse) { + aiResponse.thinking = false; stream.read_line_async( 0, null, (stream, res) => { @@ -234,7 +235,7 @@ class GPTService extends Service { } send(msg) { - this._messages.push(new GPTMessage('user', msg)); + this._messages.push(new GPTMessage('user', msg, false, true)); this.emit('newMsg', this._messages.length - 1); const aiResponse = new GPTMessage('assistant', 'thinking...', true, false)