Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# ChatAI Plasmoid

KDE Plasma 6 widget that embeds AI chat services (ChatGPT, Claude, Gemini, DuckDuckGo, DeepSeek, etc.) in a panel popup using QtWebEngine.

## Architecture

Pure QML plasmoid — no C++, no build system. Installed via KDE package manager.

```
contents/
config/
main.xml # KDE configuration schema (all settings)
config.qml # Config tab definitions (General + Appearance)
ui/
main.qml # PlasmoidItem root, popup layout, header auto-hide, WebView loader
WebView.qml # WebEngineView, permissions, downloads, context menu, JS injection
Header.qml # Navigation bar, URL selector, control buttons (Item wrapping RowLayout)
CompactRepresentation.qml # Panel icon with dynamic icon modes
ConfigGeneral.qml # Settings: sites, permissions, web features, downloads, cache
ConfigAppearance.qml # Settings: icon, effects, transparency, focus mode, header options
FindBar.qml # Ctrl+F find-in-page bar
DownloadBar.qml # Download progress/cancel/open UI
```

## Key patterns

- **Configuration**: all settings in `main.xml`, accessed via `plasmoid.configuration.propertyName`
- **Signals**: Header communicates with main.qml via signals (goBackToHomePage, navigateBackRequested, etc.)
- **JS injection**: WebView injects CSS/JS into pages for: browser spoofing, transparency, focus mode, keyboard shortcuts
- **Heuristic DOM analysis**: transparency and focus mode analyze page structure to find sidebars, headers, input areas
- **WebEngine lifecycle**: configurable keep-alive (instant reopen) or 5-min idle unload (memory saving)

## Important constraints

- `PlasmaExtras.Menu`/`MenuItem` do NOT exist in Plasma 6 — use `PlasmaComponents3.Menu`/`MenuItem`
- `PlasmaComponents3.Menu` cannot be a root type in a separate QML file (causes "Type cannot be created in QML")
- `import org.kde.notification` MUST keep version `1.0` (unversioned fails at runtime)
- `MultiEffect` blur cannot affect WebEngineView content (native surface) — use CSS injection instead
- `backgroundHints` is set via `Plasmoid.backgroundHints` (attached property), not directly on PlasmoidItem
- Header.qml root is `Item` (not `RowLayout`) — signals, properties, and functions must be on the Item, not inside the RowLayout

## Configuration properties

Settings are split across two tabs:

**General** (ConfigGeneral.qml): site toggles (showChatGPT, showClaude, etc.), custom sites, permissions (mic/webcam/screenshare/notifications/geolocation), web features (JS clipboard, spatial nav), download path, cache management, profile name, keepWebEngineAlive, spoofChromeBrowser

**Appearance** (ConfigAppearance.qml): iconMode, animations, header gradient, accent glow, focus mode, transparency + opacity slider, header visibility toggles

## JS injection layers (WebView.qml)

Applied on every page load via `onLoadingChanged`:

1. **Browser spoof** (`injectBrowserSpoof`): Chrome 130 UA, navigator.vendor/platform/plugins, window.chrome object
2. **Transparency** (`injectTransparencyCSS`): heuristic DOM walk, classifies elements as chrome/background/chat, applies layered rgba + backdrop-blur
3. **Focus mode** (`injectFocusMode`): per-site CSS rules + heuristic fallback hiding nav/aside/header/sidebar
4. **Keyboard shortcuts**: Enter-to-send for compatible services (DuckDuckGo, ChatGPT, Gemini, Claude, You)

## Testing

No automated tests. Test by:
1. Installing: `plasmashell --replace &` or remove/re-add widget
2. Check logs: `journalctl --user -u plasma-plasmashell -b | grep -i "chatai\|webview\|error"`
3. Lint: `qmllint contents/ui/*.qml` (ignore "Library import requires a version" warnings)
4 changes: 2 additions & 2 deletions contents/config/config.qml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/

import QtQuick
import org.kde.plasma.configuration
import QtQuick 2.0
import org.kde.plasma.configuration 2.0

ConfigModel {
ConfigCategory {
Expand Down
120 changes: 37 additions & 83 deletions contents/config/main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,52 +8,21 @@
<entry name="url" type="String">
<default>https://duckduckgo.com/chat</default>
</entry>
<entry name="lastVisitedUrl" type="String">
<default></default>
</entry>
<entry name="icon" type="String">
<label>i18n("The name of the icon used in the compact representation (e.g. on a small panel).")</label>
<default></default>
</entry>
<entry name="iconMode" type="Int">
<label>i18n("Icon display mode (0: Favicon, 1: Adaptive, 2: Dark, 3: Light, 4: Outlined, 5: Filled, 6: Colorful, 7: Custom)")</label>
<label>i18n("Icon display mode (0: Favicon, 1: Adaptive, 2: Dark, 3: Light, 4: Outlined, 5: Filled, 6: Colorful)")</label>
<default>1</default>
</entry>
<entry name="customIcon" type="String">
<label>i18n("Custom icon name or path")</label>
<default>help-about</default>
</entry>
<entry name="useFilledChatIcon" type="Bool">
<label>i18n("Use the filled chat's icon instead of widget icon")</label>
<default>false</default>
</entry>
<entry name="useOutlinedChatIcon" type="Bool">
<label>Use the outline chat's icon instead of widget icon</label>
<default>false</default>
</entry>
<entry name="useColorfulChatIcon" type="Bool">
<label>Use the colorful chat's icon instead of widget icon</label>
<default>false</default>
</entry>
<entry name="useDefaultIcon" type="Bool">
<label>Use the widget's default adaptive icon</label>
<default>true</default>
</entry>
<entry name="useDefaultLightIcon" type="Bool">
<label>Use the widget's default light icon</label>
<default>false</default>
</entry>
<entry name="useDefaultDarkIcon" type="Bool">
<label>Use the widget's default dark icon</label>
<default>false</default>
</entry>
<entry name="useFavicon" type="Bool">
<label>Use website favicon as widget icon</label>
<default>false</default>
</entry>
<entry name="favIcon" type="String">
<label>Base64 encoded favicon from website</label>
<default></default>
</entry>
<entry name="lastFavIcon" type="String">
<label>Last successfully loaded favicon URL</label>
<default></default>
</entry>
<entry name="showDuckDuckGoChat" type="Bool">
Expand Down Expand Up @@ -104,16 +73,13 @@
<entry name="hideHeader" type="Bool">
<default>false</default>
</entry>
<entry name="hideGoToButton" type="Bool">
<default>false</default>
</entry>
<entry name="hideKeepOpen" type="Bool">
<default>false</default>
</entry>
<entry name="hideCustomURL" type="Bool">
<default>false</default>
</entry>
<entry name="hidePrintButton" type="Bool">
<entry name="hideAutoHideButton" type="Bool">
<default>false</default>
</entry>
<entry name="hideCloseButton" type="Bool">
Expand Down Expand Up @@ -144,94 +110,82 @@
<default></default>
</entry>
<entry name="customSites" type="String">
<label>i18n("List of custom sites")</label>
<default></default>
</entry>
<entry name="keepOpen" type="Bool">
<default>false</default>
</entry>
<entry name="pin" type="Bool">
<default>false</default>
</entry>
<entry name="loadOnStartup" type="Bool">
<label>i18n("Load website on Plasma startup")</label>
<default>false</default>
</entry>
<entry name="keepWebEngineAlive" type="Bool">
<default>true</default>
</entry>
<entry name="spatialNavigationEnabled" type="Bool">
<label>Enable spatial navigation for keyboard-based browsing</label>
<default>false</default>
</entry>
<entry name="javascriptCanPaste" type="Bool">
<label>Allow JavaScript to paste from clipboard</label>
<default>true</default>
</entry>
<entry name="javascriptCanOpenWindows" type="Bool">
<label>Allow JavaScript to open new windows</label>
<default>true</default>
</entry>
<entry name="javascriptCanAccessClipboard" type="Bool">
<label>Allow JavaScript to access clipboard</label>
<default>true</default>
</entry>
<entry name="allowUnknownUrlSchemes" type="Bool">
<label>Allow unknown URL schemes</label>
<default>true</default>
</entry>
<entry name="playbackRequiresUserGesture" type="Bool">
<label>Require user gesture for media playback</label>
<default>false</default>
</entry>
<entry name="focusOnNavigationEnabled" type="Bool">
<label>Enable focus on navigation</label>
<default>true</default>
</entry>
<entry name="centerOnScreen" type="Bool">
<label>Center window on screen when opened</label>
<entry name="notificationsEnabled" type="Bool">
<default>true</default>
</entry>
<entry name="geolocationEnabled" type="Bool">
<default>false</default>
</entry>
<entry name="dialogWidth" type="Int">
<label>Last saved dialog width</label>
<default>0</default>
<entry name="autoHideHeader" type="Bool">
<default>false</default>
</entry>
<entry name="dialogHeight" type="Int">
<label>Last saved dialog height</label>
<default>0</default>
<entry name="webEngineProfileName" type="String">
<default>chat-ai</default>
</entry>
<entry name="notificationsEnabled" type="Bool">
<label>Allow notifications from websites</label>
<default>true</default>
<entry name="overlayOpacity" type="Double">
<default>0.85</default>
</entry>
<entry name="notificationFlags" type="Int">
<label>Notification display flags</label>
<default>0</default>
<entry name="enableAnimations" type="Bool">
<default>true</default>
</entry>
<entry name="notificationUrgency" type="Int">
<label>Notification urgency level</label>
<default>1</default>
<entry name="backgroundTransparency" type="Double">
<default>0.85</default>
</entry>
<entry name="notificationTimeout" type="Int">
<label>Notification timeout in milliseconds (0 for no timeout)</label>
<default>5000</default>
<entry name="enableBlur" type="Bool">
<label>Enable blur effect (desktop + web page backgrounds)</label>
<default>true</default>
</entry>
<entry name="geolocationEnabled" type="Bool">
<label>Allow websites to access geolocation</label>
<entry name="headerGradient" type="Bool">
<label>Show gradient effect on header</label>
<default>false</default>
</entry>
<entry name="autoHideHeader" type="Bool">
<label>Auto-hide header and show on mouse hover</label>
<entry name="accentBorder" type="Bool">
<label>Show accent glow around the widget</label>
<default>false</default>
</entry>
<entry name="cachePath" type="String">
<label>Cache directory path</label>
<default></default>
<entry name="spoofChromeBrowser" type="Bool">
<label>Disguise as Chrome browser for better site compatibility</label>
<default>true</default>
</entry>
<entry name="clearCacheOnExit" type="Bool">
<label>Clear cache when closing</label>
<entry name="focusMode" type="Bool">
<label>Hide sidebars and navigation for a cleaner chat view</label>
<default>false</default>
</entry>
<entry name="webEngineProfileName" type="String">
<label>Storage name used for Qt WebEngine profile</label>
<default>chat-ai</default>
<entry name="newChatShortcut" type="String">
<label>Keyboard shortcut to open a new chat (e.g. Ctrl+Shift+O, Ctrl+N)</label>
<default>Ctrl+Shift+O</default>
</entry>
</group>
</kcfg>
61 changes: 25 additions & 36 deletions contents/ui/CompactRepresentation.qml
Original file line number Diff line number Diff line change
@@ -1,40 +1,23 @@
/*
* SPDX-FileCopyrightText: 2024 Denys Madureira <denysmb@zoho.com>
* SPDX-FileCopyrightText: 2025 Bruno Gonçalves <bigbruno@gmail.com>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/

import QtQuick
import QtQuick.Layouts

import org.kde.plasma.core as PlasmaCore
import org.kde.kirigami as Kirigami
import org.kde.plasma.plasmoid
import org.kde.plasma.core as PlasmaCore

Item {
id: compactRoot

// Icon mode constants (must match ConfigAppearance.qml ComboBox order)
readonly property int iconModeFavicon: 0
readonly property int iconModeAdaptive: 1
readonly property int iconModeDark: 2
readonly property int iconModeLight: 3
readonly property int iconModeOutlined: 4
readonly property int iconModeFilled: 5
readonly property int iconModeColorful: 6
readonly property int iconModeCustom: 7

property var models: []
property var webview: null
property string fallbackIcon: "help-about"

readonly property bool isVertical: plasmoid.formFactor === PlasmaCore.Types.Vertical

Layout.minimumWidth: isVertical ? 0 : Kirigami.Units.iconSizes.medium
Layout.minimumHeight: isVertical ? Kirigami.Units.iconSizes.medium : 0
Layout.minimumWidth: isVertical ? 0 : Kirigami.Units.iconSizes.large
Layout.minimumHeight: isVertical ? Kirigami.Units.iconSizes.large : 0

implicitWidth: Kirigami.Units.iconSizes.medium
implicitHeight: Kirigami.Units.iconSizes.medium
implicitWidth: Kirigami.Units.iconSizes.large
implicitHeight: Kirigami.Units.iconSizes.large

MouseArea {
id: mouseArea
Expand All @@ -48,6 +31,15 @@ Item {
source: getIconSource()
}

// Direct webview connection for loading state changes
Connections {
target: compactRoot.webview
enabled: compactRoot.webview !== null
function onLoadingChanged(loadingInfo) {
// Placeholder for potential future use
}
}

function getIconSource() {
let icon = getIconNameOrPath();
if (icon.indexOf("/") !== -1 || icon.endsWith(".svg") || icon.endsWith(".png")) {
Expand All @@ -63,48 +55,45 @@ Item {
const currentModel = models.find(model => plasmoid.configuration.url.includes(model.url));
const colorContrast = getBackgroundColorContrast();

// Colorful mode. If not in colorful mode, some models only have colorful icons available
const hasOnlyColorfulIcon = mode !== iconModeColorful && ["lobechat", "bigagi"].includes(currentModel?.id);
// Mode 6 is Colorful. If not in colorful mode, some models only have colorful icons available
const hasOnlyColorfulIcon = mode !== 6 && ["lobechat", "bigagi"].includes(currentModel?.id);

if (!currentModel || currentModel?.id === "blackbox" || hasOnlyColorfulIcon) {
return `assets/logo-${colorContrast}.svg`;
}

if (currentModel.useIcon) {
const style = mode === iconModeFilled ? "filled" : "outlined";
const style = mode === 5 ? "filled" : "outlined";
return `assets/${style}/${currentModel.useIcon}-${colorContrast}.svg`;
}

if (mode === iconModeColorful) {
if (mode === 6) {
return `assets/colorful/${currentModel.id}.svg`;
}

const style = mode === iconModeFilled ? "filled" : "outlined";
const style = mode === 5 ? "filled" : "outlined";
return `assets/${style}/${currentModel.id}-${colorContrast}.svg`;
}

function getIconNameOrPath() {
const mode = plasmoid.configuration.iconMode;

if (mode === iconModeCustom) {
return plasmoid.configuration.customIcon || fallbackIcon;
}

if (mode === iconModeFavicon) {
if (mode === 0) { // Favicon
const faviconUrl = plasmoid.configuration.favIcon || plasmoid.configuration.lastFavIcon;
if (faviconUrl) {
return faviconUrl.replace("image://favicon/", "");
}
}

if (mode >= iconModeOutlined) {
if (mode >= 4) { // Outlined, Filled, Colorful
return getChatModelIcon() || fallbackIcon;
}

const contrast = getBackgroundColorContrast();
if (mode === iconModeDark) return "assets/logo-dark.svg";
if (mode === iconModeLight) return "assets/logo-light.svg";
if (mode === 2) return "assets/logo-dark.svg";
if (mode === 3) return "assets/logo-light.svg";

// Mode 1 or fallback
return `assets/logo-${contrast}.svg`;
}

Expand Down
Loading