diff --git a/gui/main-kf5.qml b/gui/main-kf5.qml index 029c70d..b468775 100644 --- a/gui/main-kf5.qml +++ b/gui/main-kf5.qml @@ -39,6 +39,7 @@ ApplicationWindow { dark: theme.darkColor light: theme.lightColor shadow: theme.shadowColor + placeholderText: theme.placeholderTextColor } // --- Settings Dialog --- @@ -179,9 +180,77 @@ ApplicationWindow { } function openNewSessionDialog() { - var path = theme.chooseFolder("Choose working directory for new session") - if (path !== "") - dbus.createSession(path) + newSessionDialog.backendModel = dbus.listBackends() + newSessionDialog.cwdField.text = dbus.activeCwd || "" + newSessionDialog.backendCombo.currentIndex = 0 + newSessionDialog.modelCombo.editText = "" + newSessionDialog.open() + } + + Dialog { + id: newSessionDialog + title: "New Session" + modal: true + anchors.centerIn: parent + width: 450 + standardButtons: Dialog.Ok | Dialog.Cancel + + property alias cwdField: cwdInput + property alias backendCombo: backendSelect + property alias modelCombo: modelSelect + property var backendModel: [] + + onAccepted: { + var cwd = cwdInput.text.trim() + if (cwd === "") cwd = dbus.activeCwd || "" + var backend = backendSelect.currentText + var model = modelSelect.editText.trim() || modelSelect.currentText + dbus.createSession(cwd, backend, model) + } + + GridLayout { + columns: 2 + width: parent.width + columnSpacing: 10 + rowSpacing: 8 + + Label { text: "Working directory:" } + RowLayout { + Layout.fillWidth: true + TextField { + id: cwdInput + Layout.fillWidth: true + placeholderText: "/home/user/project" + } + Button { + text: "Browse" + onClicked: { + var path = theme.chooseFolder("Choose working directory", cwdInput.text) + if (path !== "") + cwdInput.text = path + } + } + } + + Label { text: "Backend:" } + ComboBox { + id: backendSelect + Layout.fillWidth: true + model: newSessionDialog.backendModel + onCurrentTextChanged: { + if (currentText) + modelSelect.model = dbus.listModelsByBackend(currentText) + } + } + + Label { text: "Model:" } + ComboBox { + id: modelSelect + Layout.fillWidth: true + editable: true + editText: "" + } + } } function openCwdDialog() { @@ -209,6 +278,15 @@ ApplicationWindow { property real panX: 0 property real panY: 0 property real zoom: 1.0 + property real minZoom: 0.3 + property real maxZoom: 3.0 + + function stateColor(state) { + if (state === "idle") return "#4caf50" + if (state && state.startsWith("calling")) return "#ff9800" + if (state === "thinking") return "#2196f3" + return "#2196f3" + } function layoutNodes() { var positions = {} @@ -257,7 +335,13 @@ ApplicationWindow { } onWheel: { var factor = wheel.angleDelta.y > 0 ? 1.15 : 0.87 - graphRoot.zoom = Math.max(0.3, Math.min(3.0, graphRoot.zoom * factor)) + var newZoom = Math.max(graphRoot.minZoom, Math.min(graphRoot.maxZoom, graphRoot.zoom * factor)) + // Zoom toward mouse position + var mx = wheel.x - graphRoot.panX + var my = wheel.y - graphRoot.panY + graphRoot.panX -= mx * (newZoom / graphRoot.zoom - 1) + graphRoot.panY -= my * (newZoom / graphRoot.zoom - 1) + graphRoot.zoom = newZoom } } @@ -324,12 +408,7 @@ ApplicationWindow { height: 50 radius: 8 color: palette.base - border.color: { - var st = modelData.state - if (st === "idle") return "#4caf50" - if (st && st.startsWith("calling")) return "#ff9800" - return "#2196f3" - } + border.color: graphRoot.stateColor(modelData.state) border.width: modelData.id === dbus.activeSessionId ? 3 : 2 x: { var pos = graphRoot.nodePositions[modelData.id] @@ -364,12 +443,7 @@ ApplicationWindow { font.pixelSize: 9 opacity: 0.7 anchors.horizontalCenter: parent.horizontalCenter - color: { - var st = modelData.state - if (st === "idle") return "#4caf50" - if (st && st.startsWith("calling")) return "#ff9800" - return "#2196f3" - } + color: graphRoot.stateColor(modelData.state) } } @@ -707,6 +781,24 @@ ApplicationWindow { mainContent.panes[idx].sessionId = dbus.activeSessionId } } + function onErrorOccurred(message) { + errorDialog.text = message + errorDialog.open() + } + } + + Dialog { + id: errorDialog + title: "Error" + modal: true + anchors.centerIn: parent + standardButtons: Dialog.Ok + property alias text: errorLabel.text + Label { + id: errorLabel + wrapMode: Text.Wrap + width: parent.width + } } // --- Inline ChatPane Component --- @@ -1251,6 +1343,7 @@ ApplicationWindow { sourceComponent: subBlock.type === "code" ? codeSubDelegate : subBlock.type === "diff" ? diffSubDelegate : subBlock.type === "interrupt" ? interruptSubDelegate + : subBlock.type === "video" ? videoSubDelegate : proseSubDelegate } } @@ -1315,6 +1408,7 @@ ApplicationWindow { sourceComponent: subBlock.type === "code" ? codeSubDelegate : subBlock.type === "diff" ? diffSubDelegate : subBlock.type === "interrupt" ? interruptSubDelegate + : subBlock.type === "video" ? videoSubDelegate : proseSubDelegate } } @@ -1414,6 +1508,7 @@ ApplicationWindow { sourceComponent: subBlock.type === "code" ? codeSubDelegate : subBlock.type === "diff" ? diffSubDelegate : subBlock.type === "interrupt" ? interruptSubDelegate + : subBlock.type === "video" ? videoSubDelegate : plainSubDelegate } } @@ -1475,6 +1570,7 @@ ApplicationWindow { sourceComponent: subBlock.type === "code" ? codeSubDelegate : subBlock.type === "diff" ? diffSubDelegate : subBlock.type === "interrupt" ? interruptSubDelegate + : subBlock.type === "video" ? videoSubDelegate : plainSubDelegate } } @@ -1591,6 +1687,78 @@ ApplicationWindow { } } + Component { + id: videoSubDelegate + Item { + id: videoItem + width: parent ? parent.width : 400 + height: Math.max(200, Math.min(width * 9/16, 360)) + + property string videoUrl: subBlock.content || "" + property bool isDirectMedia: /\.(mp4|webm|ogv|m3u8)(\?|$)/i.test(videoUrl) + property string youtubeId: { + var m = videoUrl.match(/[?&]v=([^&]+)/) || videoUrl.match(/youtu\.be\/([^?]+)/) + return m ? m[1] : "" + } + + // Non-direct: thumbnail card that opens in browser + Rectangle { + anchors.fill: parent + color: "#181818" + radius: 8 + clip: true + + // YouTube thumbnail + Image { + anchors.fill: parent + source: videoItem.youtubeId !== "" ? + "https://img.youtube.com/vi/" + videoItem.youtubeId + "/hqdefault.jpg" : "" + fillMode: Image.PreserveAspectCrop + visible: videoItem.youtubeId !== "" + } + + // Play button overlay + Rectangle { + anchors.centerIn: parent + width: 72; height: 72 + radius: 36 + color: "#aa000000" + Text { + anchors.centerIn: parent + text: "\u25B6" + font.pixelSize: 36 + color: "white" + } + } + + // URL label at bottom + Rectangle { + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + height: 28 + color: "#cc000000" + radius: 4 + Text { + anchors.centerIn: parent + width: parent.width - 16 + text: videoItem.videoUrl + color: "#ccc" + font.pixelSize: 11 + elide: Text.ElideMiddle + horizontalAlignment: Text.AlignHCenter + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: Qt.openUrlExternally(videoItem.videoUrl) + } + } + } + } + Component { id: codeSubDelegate Rectangle {